Profile

i love cat

as3617

Codegate 2024 quals - Cha's Wall writeup

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

image

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