MODELS
Unlimited Basic subscription: unlimited Qwen 3.8 27B for €14.95/month. GLM + DeepSeek with 5,000 shared AI Credits. Fair use applies.

Technical guide · Private models for cybersecurity

AI for cybersecurityReview PHP, test a patch and investigate SSH logins

Deploy a private abliterated model for cybersecurity on QDivZero. Follow SQL and PHP tests, analyze SSH logs, and estimate compute costs with working examples.

QDivZero Team12 min read
Deploy the modelCompute documentationRead the guide ↓
The Great Wall among misty mountains at sunrise, with the Spanish title Ciberseguridad
Guide contents7 sections

We tested a private AI model on QDivZero with three cybersecurity tasks. We reproduced a SQL injection, analyzed seven SSH events and compiled a patch it proposed for PHP. The PHP review took two attempts; we rejected the first answer.

Follow the steps below with the supplied code, original answers and screenshots. SQL and SSH are lab exercises; PHP uses original 8.2.34 source and tests run in local containers.

On 21 October 2026, the assigned capacity cost €0.41/hour. Across two sessions, the deployment was on for about 45 minutes, approximately €0.31 in estimated compute. That includes startup and pauses; it is not a per-query price.

1. Download the files

You need a QDivZero account with credit and Python 3.11 or later. Download laboratorio.zip, extract it and open a terminal in that folder. There are no packages to install.

FilePurpose
review_security.pySend a case to the model and save its answer
cases/code_review.py.txtCustomer lookup code
sql_lab.pyReproduce the injection in a local database
cases/ssh_events.logSSH login events
cases/php82_session_excerpt.c.txtOriginal source for the PHP review
reference.mdResults and limits of all three tests

The client sends text and saves answers. sql_lab.py runs the SQL test on your computer. Rebuilding PHP also requires Docker and the php-liferaft environment in section 5. The three cases are independent; you can start with SQL or SSH and leave compilation for later.

2. Deploy and start the model

We used hotdogs/Qwen3.8-27B-abliterated-MTP-GGUF, a community version of Qwen 3.8 27B. Its repository lists an Apache 2.0 license.

  1. Open Deployments → Deploy model → Choose manually and search for that repository.
  2. Under Select GGUF quantization → Q4_K_M, choose Qwen3.8-27B-abliterated-mtp-Q4_K_M-mixed.gguf.
  3. Under Pod, select Custom → Allow reduced context and enter 8192. The exercise files fit within this window.
  4. Keep Smart scheduling. To use European capacity only, select Europe and deselect No region. We disabled community and interruptible capacity for our test.
  5. Set your Maximum price per hour. We entered €1/hour; this caps the hourly rate, not the total budget. Under Scheduling, add a Stop rule for the end of your session.
  6. Under Configuration, enter Guía de ciberseguridad as the display name. Leave Serverless and the replica set disabled.
  7. Under Review → Model name, enter ciberseguridad-qwen. This is the identifier the Python client will use.
  8. Click Deploy model. Open its row and choose Model status → Start model.
Private model configuration in QDivZero with an 8192-token context and Europe selected, shown in Spanish
Deployment review in QDivZero's Spanish interface. The estimate was €0.43/hour; the capacity eventually assigned showed €0.41/hour. Click any screenshot to enlarge it and browse the slideshow.

Not started means the deployment exists but is off. While it shows Pending, wait for startup to finish.

Abliterated refers to a weight modification that reduces model refusals. The paper on the refusal direction explains the technique. This does not add cybersecurity knowledge by itself.

The selected GGUF file is about 17.8 GB. QDivZero runs it with llama.cpp and needs additional memory for the context and runtime. Although its name includes MTP, the runtime reviewed for this guide does not automatically enable that speculative decoding feature.

3. Send your first request

With the model running, open Developers → Chat and select ciberseguridad-qwen. Leave Developer message empty and set Thinking → Disabled.

Send this message.

1Reply with the word READY only.
2

Once you receive READY, the endpoint is responding. To call it from your terminal, create an API key and export these variables.

1export QDIV0_API_KEY='YOUR_API_KEY'
2export QDIV0_ACCOUNT_ID='YOUR_ACCOUNT_ID'
3

The account ID appears under Developers → View code, in the X-Account-Id header. The client calls https://api.qdiv0.com/v1/chat/completions with the name ciberseguridad-qwen.

The commands below print the answer in your terminal and save a JSON file under results. If you repeat an exercise, choose a new output filename to keep the previous result. The client sends one user message with reasoning_effort: "none" and a 3072-token output limit. If the answer is cut short, it prints a warning and sets truncated in the JSON file.

The published answers came from QDivZero's chat, using the saved messages in prompts/. The bundled client repeats those requests through the API; we validated it with local HTTP tests. Chat streamed the answer as it arrived; the script uses stream: false and waits for the complete JSON response.

If you chose a different serving name, set export QDIV0_MODEL='your-name'. The display name in the deployment list does not replace that identifier. On an availability error, inspect deployment status before sending another request; the script does not retry automatically.

4. Red team with a SQL injection

The first file contains two versions of a name lookup, one using interpolation and one using parameters. We sent both to the model and asked for an input and a prediction for each version. This is the vulnerable function from cases/code_review.py.txt.

1def find_customer(connection, name):
2    query = (
3        "SELECT id, name, email FROM customers "
4        f"WHERE name = '{name}'"
5    )
6    return connection.execute(query).fetchall()
7

The f-string inserts the function argument into the query. Alice works, but the apostrophe in O'Connor already breaks this construction. Demonstrating injection also requires an input that changes which rows are returned.

The task asks for a minimal in-memory SQLite demonstration with two customers and the expected result for the same input in the parameterized query. Read it in prompts/redteam.txt; this command sends it with the code.

1python3 review_security.py --case redteam --language en --output results/redteam.json
2

In our test, the model returned a Python program demonstrating the issue in memory. Download its original response. It used this input.

1' OR 1=1 --
2

Substituting the input produces this query. OR 1=1 makes the condition true for every row; -- comments out the closing quote appended by the function.

1SELECT id, name, email FROM customers WHERE name = '' OR 1=1 -- '
2

We ran the same input in the bundled lab. You can repeat this test without starting the model or spending QDivZero compute.

1python3 sql_lab.py
2

It creates an in-memory SQLite database with two customers, Alice and O'Connor, and sends the same input to the vulnerable and fixed functions. This is the output.

1{
2  "database": ":memory:",
3  "proof_input": "' OR 1=1 -- ",
4  "vulnerable_rows": 2,
5  "parameterized_rows": 0
6}
7

The lookup returned Alice and O'Connor, although we asked for neither name. Those are the 2 rows in vulnerable_rows. The parameterized function returns 0 because it searches for the complete text as a name and finds no match.

The corrected function uses SQLite's ? placeholder and passes the name as a one-element tuple.

1def find_customer(connection, name):
2    query = "SELECT id, name, email FROM customers WHERE name = ?"
3    return connection.execute(query, (name,)).fetchall()
4

SQLite receives the query and value separately. The contents of name can no longer become a SQL condition. The lab also tests a legitimate lookup for O'Connor and a missing name.

The program asserts these results and closes the database afterwards.

5. A PHP case, from source to patch

This test began with the report about session.upload_progress and the backport work in php-liferaft.

During this test, on the morning of 21 October 2026, PHP's official announcement scheduled two critical fixes for 14:00 UTC, 16:00 in Madrid. The report attributed the issue to two CVEs; that attribution and its exploitation chain still needed corroboration. We started with an incorrect counter reproduced in PHP 8.2.34.

An upload finished with an incorrect counter

We upload two test files followed by a large form field. The complete body is 262,642 bytes, but progress saved in the session stops at 136,576. The upload finishes and the individual file sizes are correct. The final counter has fallen behind.

To investigate, we use selected original C source from before the patch. The ZIP includes cases/php82_session_excerpt.c.txt. It preserves original locations using #line and contains parts of session.c and php_session.h; these excerpts cannot be compiled as a standalone file.

The multipart body contained a 131,072-byte file, a 31-byte file and a trailing 131,072-byte form field. Boundaries and headers brought it to 262,642 bytes. We used these settings to retain the result in the session and update it frequently.

1session.upload_progress.cleanup=0
2session.upload_progress.freq=1
3session.upload_progress.min_freq=0
4

With cleanup=0, progress remains in the session after completion. The test requires done=true and bytes_processed equal to the complete body length.

We selected 155 lines from the official PHP 8.2.34 archive. ext/session/php_session.h:136 declares post_bytes_processed as a pointer to zval. ext/session/session.c:3091 stores the address of the array field. The final block at lines 3163–3166 separates the array, then writes through that pointer. The supplied excerpt also retains the reference increment at lines 2988–2989; omitting it would change the array lifetime analysis.

Two requests and a rejected first answer

The first review, without supplying the test result, went wrong. It suggested a memory leak without demonstrating one and proposed a test that would print OK both before and after. We retained that first response and rejected it.

The first task began as follows. prompts/php.txt contains the complete message and numbered source.

1Review these selected C source excerpts from PHP 8.2.34.
2Identify a concrete defect supported by the code, cite the original
3file and line numbers, and trace the operations that cause it.
4

Repeat it separately to retain both results.

1python3 review_security.py --case php --language en --output results/php-first.json
2

We repeated the request in a fresh conversation. This time we supplied the code and observed regression data, without showing the reference patch.

1python3 review_security.py --case php-counter --language en --output results/php.json
2

prompts/php-counter.txt adds the measurements, PHP settings and correct file sizes. It asks for a minimal diff preserving progress, cleanup and cancellation. The model receives the measured symptom; neither request includes the article, CVEs or reference patch.

The model identifies SEPARATE_ARRAY(&progress->data). This operation can copy the array, but post_bytes_processed retains the address of a field in the previous array. The counter is written into that previous copy while the new one is published.

Its proposal is to look up the field again after separating the array and before writing the final value.

1SEPARATE_ARRAY(&progress->data);
2progress->post_bytes_processed = zend_hash_str_find(
3    Z_ARRVAL(progress->data), "bytes_processed",
4    sizeof("bytes_processed") - 1);
5
Real QDivZero response identifying array separation and proposing a refreshed pointer to the PHP counter, in Spanish
Second review, supplied with the incorrect counter result. The original response and diff are preserved unedited. The live request and answer were in Spanish. Click to enlarge.

Download the full response and the patch prepared for application. Its explanation also speculates that the parser fails to count the last field; the test does not support that part.

Applying the patch and testing the result

The original diff pointed to line 137 of the excerpt instead of line 3161 of the full file. We corrected the headers and indentation, preserving the three proposed C lines. From the original PHP source root, with the downloaded patch in that directory, apply it with these commands.

1patch --dry-run --batch --fuzz=0 -p1 -i php-counter-proposal.patch
2patch --batch --fuzz=0 -p1 -i php-counter-proposal.patch
3

We compiled with the local php-liferaft SDK matching the runtime and architecture. The project's build script compares source and SDK ABI identities before compiling. Inside that container, in ext/session, it runs these commands.

1phpize
2export CFLAGS="-O2 -g -fcommon"
3./configure --with-php-config=/usr/local/bin/php-config --enable-session=shared
4make -j 4
5

The result is modules/session.so. Repeating this step requires the SDK and runtimes built from the commit used for this test; the commands above assume that environment. The ZIP includes the patch and results, not a binary module interchangeable with any PHP installation.

We loaded only the new session.so into the original runtime and repeated the same upload.

Version testedFinal counterBody length
Original PHP 8.2.34136,576262,642
Independent php-liferaft patch262,642262,642
Model proposal, with corrected diff formatting262,642262,642

This was repeated with the php, php_binary and php_serialize serializers. Normal uploads, cleanup and cancellation checks also passed. Parameters and results are in php-regression.json; the reference project contains the build environment and its tests.

This check demonstrates the corrected counter in that case. It does not demonstrate remote code execution or turn the proposal into PHP's official patch. Check the official advisory and your application's PHP version before updating a server. The independent php-liferaft patch has a different scope: it removes persistent counter pointers rather than refreshing one at the end.

6. Blue team with an SSH log

For log analysis, we prepared cases/ssh_events.log with seven fictional events. Failures and a successful password login share an IP; a separate public-key login comes from another address. This lets us assess whether the model preserves details and distinguishes a suspicious sequence from a confirmed incident.

UTC timeEvent
08:00:01–08:00:31Four failures from 203.0.113.42, against admin, root and backup
08:00:42Password accepted for backup from the same IP
08:00:43Session opened for backup
08:05:10Public-key login for deploy from 192.0.2.10

The task in prompts/logs.txt asks for a timeline with timestamps and IPs, observed facts, hypotheses and further evidence to obtain. It also asks the model not to infer country, reputation or CVEs from the addresses. The command sends that task and the log lines in a single user message.

1python3 review_security.py --case logs --language en --output results/ssh.json
2

The model connected the failures with the later backup login. It also noted that we needed to establish whether the login was expected and what happened during the session. Download its full analysis. Our live requests asked for Spanish answers; the commands here request English.

Real model response in QDivZero showing the SSH event timeline in Spanish
Response from QDivZero's chat using the lab's SSH file. Developer message empty and thinking disabled. The interface and answer are in Spanish.

The events at 08:00:42 and 08:00:43 share PID 125, linking the accepted password to the opened backup session. Earlier attempts have different PIDs. The log does not prove that someone guessed the password; mistakes by a legitimate user also fit. The deploy login comes from another IP, with no evidence linking it to backup.

For an equivalent investigation, we would ask the owner of backup to confirm the 08:00:42 login, then look for session closure and activity in available records. This file ends at 08:05:10 and contains no executed commands. Both IPs belong to documentation ranges.

The facts in reference.md let you compare your answer with the source. If the model declares an attack confirmed or describes later actions absent from the file, it has gone beyond the evidence.

7. Calculate the cost and stop the GPU

After finishing your requests, return to Deployments, enter the model's row and choose Model status → Stop model. Wait for Stopped. The instance consumes compute while it is on, including between queries.

In the first session, about 33 minutes passed between the start and stop commands. At €0.41/hour, that works out to approximately €0.23 in estimated compute. This is a session calculation, not a checked invoice amount; we did not measure the exact start and end of billing.

The additional PHP review session lasted about 12 minutes between those commands, approximately €0.08 at the same rate. The two sessions total about €0.31 estimated.

Cost depends on running hours, even if you send few queries.

Time runningCalculationEstimated cost
Half an hour in the lab0.5 h × €0.41€0.21
One 8-hour workday8 h × €0.41€3.28
A month of 22 eight-hour workdays176 h × €0.41€72.16
30 days without stopping720 h × €0.41€295.20

The €0.41/hour rate belongs to our deployment with an 8192-token context. Different capacity may have a different rate. Include startup and pauses when budgeting.

For daily use, you can schedule startup and shutdown. Serverless can start it when requests arrive; enabling it removes existing schedule rules. A request arriving while the model is off must wait for startup.

Your team can call the same name, ciberseguridad-qwen, from their tools using the account's keys and permissions. The rate is for the deployment's compute, rather than a fee for each person. Simultaneous request capacity depends on the hardware and workload; we did not measure concurrency in this test.