A quick-and-dirty (naive) PHP script to hammer a particular quiz

A quick-and-dirty (naive) PHP script to hammer a particular quiz

by Visvanath Ratnaweera -
Number of replies: 5
Picture of Particularly helpful Moodlers Picture of Translators
I am investigating how much resources a particular quiz takes. In the past it has failed once for a large class during a synchronous exam. In the meantime we have done a series of improvements and want to be certain that it won't happen the next time (for the same number of users). Which means I have to simulate the user storm.

A quick-and-dirty PHP script I found in an old post https://moodle.org/mod/forum/discuss.php?d=153580#p671844 would be ideal for our case. The fundamental question is, whether this idea could be extended so that the script will enter the particular quiz, open the five essay type questions it has and download the image in each question? Yes, it is a very special kind of a quiz. It has only five questions, all essay type, the content of each question is an A4-size PNG (which happens to be a page of the exam paper).

The initial try of the script is attached below. So far it looks promising. It is able to log in and open the quiz. Missing is the part visiting each question and ideally downloading the PNG. The latter part may be dropped, if too involved.

The second question: I ran in to "Invalid Login Token" error. Bypassed that with $CFG->disablelogintoken = true; in config.php. Could somebody show me how to get this Login Token from Moodle and paste it to the request?

The script:
===
  2 $working_folder = "/tmp";
  3 $urlbase = "https://EXAPMLE.COM";
  4 $agent = "curl-min";
  5 $debug = 1;
  6 $cookiefile = tempnam($working_folder, "cookies");
  7 $username = "USERNAME";
  8 $password = "PASSWORD";
  9
 10 // get session cookies to set up the Moodle interaction
 11 // ----------------------------------------------------
 12 $ch = curl_init();
 13 curl_setopt($ch, CURLOPT_USERAGENT, $agent);
 14 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
 15 curl_setopt($ch, CURLOPT_COOKIEFILE, $cookiefile);
 16 curl_setopt($ch, CURLOPT_COOKIEJAR, $cookiefile);
 17 curl_setopt($ch, CURLOPT_VERBOSE, $debug);
 18 // if ($debug) curl_setopt($ch, CURLOPT_STDERR, $debughandle);
 19 curl_setopt($ch, CURLOPT_URL, $urlbase . "/login/index.php");
 20 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
 21 $result = curl_exec ($ch);
 22 curl_close ($ch);
 23
 24 // login to Moodle
 25 // ---------------
 26
 27 $postfields = array(
 28 'username' => $username,
 29 'password' => $password,
 'testcookies' => '1'
 31 );
 32
 33 $ch = curl_init();
 34 curl_setopt($ch, CURLOPT_USERAGENT, $agent);
 35 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
 36 curl_setopt($ch, CURLOPT_COOKIEFILE, $cookiefile);
 37 curl_setopt($ch, CURLOPT_COOKIEJAR, $cookiefile);
 38 curl_setopt($ch, CURLOPT_VERBOSE, $debug);
 39 // if ($debug) curl_setopt($ch, CURLOPT_STDERR, $debughandle);
 40
 41 curl_setopt($ch, CURLOPT_URL, $urlbase . '/login/index.php');
 42 curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postfields));
 43 curl_setopt($ch, CURLOPT_POST, 1);
 44 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 0);
 45 curl_setopt($ch, CURLOPT_HEADER, 1);
 46 $result = curl_exec ($ch);
 47 curl_close ($ch);
 48
 49 if (!$result || !preg_match("/HTTP\/1.1 303 See Other/", $result))
 50 {
 51 unlink($cookiefile);
 52 header("HTTP/1.0 403 Forbidden");
 53 die("Username/password incorrect.\n");
 54 }
 55
 56 // get session key
 57 // ---------------
 58 $ch = curl_init();
curl_setopt($ch, CURLOPT_USERAGENT, $agent);
 60 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
 61 curl_setopt($ch, CURLOPT_COOKIEFILE, $cookiefile);
 62 curl_setopt($ch, CURLOPT_COOKIEJAR, $cookiefile);
 63 curl_setopt($ch, CURLOPT_VERBOSE, $debug);
 64 // if ($debug) curl_setopt($ch, CURLOPT_STDERR, $debughandle);
 65
 66 curl_setopt($ch, CURLOPT_URL, $urlbase . "/mod/quiz    /view.php?id=7");
 67 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
 68 $result = curl_exec ($ch);
 69 curl_close ($ch);
===

Output with debug=1 (no output with debug=0)
===
* Expire in 0 ms for 1 (transfer 0x55a806287950)
[100s of repetitions]
447 * Expire in 0 ms for 1 (transfer 0x55a806287950)
448 * Expire in 1 ms for 1 (transfer 0x55a806287950)
449 *   Trying X.X.X.X...
450 * TCP_NODELAY set
451 * Expire in 200 ms for 4 (transfer 0x55a806287950)
452 * Connected to EXAMPLE.COM (X.X.X.X) port 443 (#0)
453 * ALPN, offering h2
454 * ALPN, offering http/1.1
455 * successfully set certificate verify locations:
456 *   CAfile: none
457   CApath: /etc/ssl/certs
458 * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
459 * ALPN, server accepted to use http/1.1
460 * Server certificate:
461 *  subject: CN=EXAMPLE.COM
462 *  start date: Jan  7 17:32:26 2022 GMT
463 *  expire date: Apr  7 17:32:25 2022 GMT
464 *  subjectAltName: host "EXAMPLE.COM" matched cert's "EXAMPLE.COM"
465 *  issuer: C=US; O=Let's Encrypt; CN=R3
466 *  SSL certificate verify ok.
467 > GET /login/index.php HTTP/1.1^M
468 Host: EXAMPLE.COM^M
469 User-Agent: curl-min^M
470 Accept: */*^M
471 ^M
472 * old SSL session ID is stale, removing
473 < HTTP/1.1 200 OK^M
474 < Date: Sun, 09 Jan 2022 16:52:31 GMT^M
475 < Server: Apache^M
476 * Added cookie MoodleSession="2bkj2sg7t0e83qadtl8nrphq3l" for domain EXAMPLE.COM, path /, expire 0
477 < Set-Cookie: MoodleSession=2bkj2sg7t0e83qadtl8nrphq3l; path=/; secure^M
478 < Expires: ^M
479 < Cache-Control: private, pre-check=0, post-check=0, max-age=0, no-transform    ^M
480 < Pragma: no-cache^M
481 < Content-Language: en^M
482 < Content-Script-Type: text/javascript^M
483 < Content-Style-Type: text/css^M
484 < X-UA-Compatible: IE=edge^M
485 < Accept-Ranges: none^M
486 < X-Frame-Options: sameorigin^M
487 < Vary: Accept-Encoding^M
488 < Transfer-Encoding: chunked^M
489 < Content-Type: text/html; charset=utf-8^M
490 < ^M
491 * Connection #0 to host EXAMPLE.COM left intact
492 * Expire in 0 ms for 6 (transfer 0x55a8062a5c50)
[basically the same pattern repeats]
===

httpd-access.log
===
[Sun Jan 09 17:30:24.468256 2022] [php7:notice] [pid 762020] [client x.x.x.x:38614] Default exception handler: This quiz attempt no longer exists. Debug: \nError code: attempterrorcontentchangeforuser\n* line 2635 of /mod/quiz/locallib.php: moodle_exception thrown\n* line 44 of /mod/quiz/attempt.php: call to quiz_create_attempt_handling_errors()\n
===

Average of ratings: -
In reply to Visvanath Ratnaweera

Re: A quick-and-dirty (naive) PHP script to hammer a particular quiz

by Visvanath Ratnaweera -
Picture of Particularly helpful Moodlers Picture of Translators
I made some progress. Following the same pattern in the code, bracketed between $ch = curl_init(); and $result = curl_exec ($ch); I can successfully step through the following three URLs:
- https://EXAMPLE.COM/login/index.php // get session cookies
- https://EXAMPLE.COM/login/index.php // log in
- https://EXAMPLE.COM/mod/quiz/view.php?id=7 // open the quiz

and land on this screen:



My next question is: How do I attempt the quiz? What is the URL? The code behind the button is (HTML brackets removed and anonymized):

form method="post" action="https://EXAMPLE.COM/mod/quiz/startattempt.php
input type="hidden" name="cmid" value="7"
input type="hidden" name="sesskey" value="XXXXXXXXXXX"
button type="submit"  Attempt quiz now
/button
/form
Could somebody show me how to submit that form in PHP curl?
In reply to Visvanath Ratnaweera

Re: A quick-and-dirty (naive) PHP script to hammer a particular quiz

by Visvanath Ratnaweera -
Picture of Particularly helpful Moodlers Picture of Translators
Sorry for the moving target.
;)

Just after posting realized that what I need is something like:

$postfields = array(
   'cmid' => "7",
   'sesskey' => ??????,  //  -- where is this?
 );
 $ch = curl_init();
 [...]
 curl_setopt($ch, CURLOPT_URL, 'https://EXAMPLE.COM/mod/quiz/startattempt.php');
 curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postfields));
[...]
The question is, where is the 'sesskey'?

In reply to Visvanath Ratnaweera

Re: A quick-and-dirty (naive) PHP script to hammer a particular quiz

by Davo Smith -
Picture of Core developers Picture of Particularly helpful Moodlers Picture of Peer reviewers Picture of Plugin developers
You would need to parse the HTML of the form within the quiz in order to extract the sesskey - it'll be a hidden field with name 'sesskey' and (by design) it will be unique every time you log in to the site (it will remain the same for the length of that login session).
In reply to Davo Smith

Re: A quick-and-dirty (naive) PHP script to hammer a particular quiz

by Visvanath Ratnaweera -
Picture of Particularly helpful Moodlers Picture of Translators
Thanks for the hint! Yes, the code snippet was at the bottom of the OP:

if (!preg_match("/sesskey=(\w*)/", $result, $matches))
{
  unlink($cookiefile);
  header("HTTP/1.0 500 Internal Server Error");
  die("Could not determine sesskey.\n");
}
$sesskey = $matches[1];
With that the quiz opens. (See attachment)

My next step is to download the PNG. There is one in the question description but doesn't get loaded. Needs another curl block, I assume. And then have to walk the other four questions (not yet prepared) and download their PNGs too.

I know, it is slow progress. I'm new to this part of the web tech. If you or anybody else happen to know a code sample, I'd appreciate. I am searching the web myself.

Attachment curl-3.png