Home XCTF2022 final web solution
Post
Cancel

XCTF2022 final web solution

Sign in

赛题描述

前端题。本题需要分析index.hash.js的逻辑,分析过程还是比较繁琐痛苦的,分析前有几个问题需要思考:

  • burp抓到的密码是加密的,他的处理逻辑是什么?
  • 管理员的密码从何而来?
  • 能未授权访问吗?

分析思路

首先一眼看到一些字符串,这些字符串有助于我们判断函数的功能。从函数最后往上看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function hD() {
    var e = P("h1", {
        children: "404 Not Found"
    });
    return P($n, {
        children: P(kL, {
            store: pD, //存了一个变量
            children: ce(g2, {
                children: [P(Ln, {
                    index: !0,
                    element: P(vD, {})
                }), P(Ln, {
                    path: "/login",
                    element: P(FL, {
                        t: P2()
                    })
                }), P(Ln, {
                    path: "/logout",
                    element: P(LL, {})
                }), ce(Ln, {
                    path: "/user",
                    element: P(rD, {}),
                    children: [P(Ln, {
                        index: !0,
                        element: P(Ts, {
                            to: "/user/home",
                            replace: !0
                        })
                    }), P(Ln, {
                        path: "/user/home",
                        element: P(jL, {})
                    }), P(Ln, {
                        path: "*",
                        element: e
                    })]
                }), P(Ln, {
                    path: "*",
                    element: e
                })]
            })
        })
    })
}

login为例,这里用到了以下几个变量

1
2
3
4
5
6
7
8
9
10
11
12
P(Ln, {
    path: "/login",
    element: P(FL, {
        t: P2() 
    })
    }
)
/*==========================================================================================*/
FL = RL(AL, IL)($L)

/*==========================================================================================*/
Wx.g.login(t.au.n, $5(t.au.p + t.t).toString(), t.t)

阅读函数逻辑后,可以知道,

  • P2 产生 token
  • FL 用到了$L
  • t.t是token,t.au.n是username,t.au.p是密码
  • 搜索$5的魔数可知是md5

也就是可以得出如下结论,burp抓到的是md5(password + token)

假设在渗透时,分析到这里就足够了,但在CTF里面还完全不够!

接下来分析pD变量,这个在一开始就store了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
dD = aD({
    au: fD
}),
pD = tb(dD),

/*==========================================================================================*/

const fD = (e = {
    n: sD(),
    p: cD()
}, t)

/*==========================================================================================*/

const lD = [
        [114, 111],
        [111, 116]
    ],
    sD = () => nb(lD),
    uD = [
        [54, 52],
        [100, 102],
        [57, 51],
        [48, 97],
        [52, 51],
        [52, 50],
        [51, 53],
        [101, 97],
        [97, 51],
        [52, 97],
        [57, 56],
        [55, 99],
        [55, 101],
        [55, 49],
        [53, 98],
        [101, 102]
    ],
cD = () => nb(uD),

nb = e => String.fromCharCode(...e.flat()),

看到这里对ascii敏感的话就很清晰了,这些是一些字符串! 写个脚本转一下,答案就是 root64df930a434235eaa34a987c7e715bef 登录一下完事,然后根据tips以HTTP3访问index.hash.js,发现新的路由,访问一下就得到flag了。

动态调试

我们也可以用动态调试的办法去做:

首先Redux中的Store有以下职责

  • 维持应用的状态
  • 提供getState()方法获取应用状态
  • 提供dispatch(action)方法更新应用状态
  • 通过subscribe(listener)注册监听器
  • 通过subscribe(listener)返回的函数注销监听器

一个网上的例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)
import {
  addTodo,
  toggleTodo,
  setVisibilityFilter,
  VisibilityFilters
} from './actions'

// 打印初始状态
console.log(store.getState())

// 每次 state 更新时,打印日志
// 注意 subscribe() 返回一个函数用来注销监听器
const unsubscribe = store.subscribe(() =>
  console.log(store.getState())
)

// 发起一系列 action
store.dispatch(addTodo('Learn about actions'))
store.dispatch(addTodo('Learn about reducers'))
store.dispatch(addTodo('Learn about store'))
store.dispatch(toggleTodo(0))
store.dispatch(toggleTodo(1))
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))

// 停止监听 state 更新
unsubscribe();

给几个pD打上断点,运行一下就可断住。当出现下图时, 此时在console可以使用pD.getState()获取当前的应用状态,里面存着root的账号和密码

能否未授权

/user/home显然是一个值得注意的点,我们跟踪一下,发现在远程会展示Tips: Loading...的内容,看起来没法未授权。估计题目在后端判断了一下,真正的tips需要获取用户名和密码才行,相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function $_() {
    return Ua.get("/info")
}

/*==========================================================================================*/

const I_ = Object.freeze(Object.defineProperty({
        __proto__: null,
        login: M_,
        info: $_
        ...
}))

/*==========================================================================================*/
jL = () => {
    const t = ja(),
        [e, n] = m.exports.useState("Loading...");
    return m.exports.useEffect(() => {
        Wx.g.info().then(e => {
            n(e.data)
        }).catch(e => {
            Hv.error({
                message: "Error",
                description: e.message,
                duration: 10,
                placement: "topLeft"
            }), 401 == e.code && t("/")
        })
    }, []), P("div", {
        className: zL.container,
        children: ce("h1", {
            children: ["Tips: ", e, "?"]
        })
    })
}
This post is licensed under CC BY 4.0 by the author.